From 7fdfb855d606571518b781e50287090d7efbb875 Mon Sep 17 00:00:00 2001
From: Liam Newman <bitwiseman@gmail.com>
Date: Tue, 22 Apr 2025 20:20:14 -0700
Subject: [PATCH 1/4] Refactor pagination

---
 .../org/kohsuke/github/GHAppInstallation.java |   3 +-
 .../github/GHAppInstallationRequest.java      |   1 -
 .../github/GHAppInstallationsIterable.java    |  51 +--
 .../github/GHAppInstallationsPage.java        |   7 +-
 .../kohsuke/github/GHArtifactsIterable.java   |  53 +---
 .../org/kohsuke/github/GHArtifactsPage.java   |  21 +-
 .../GHAuthenticatedAppInstallation.java       |   3 +-
 .../kohsuke/github/GHCheckRunsIterable.java   |  51 +--
 .../org/kohsuke/github/GHCheckRunsPage.java   |  21 +-
 .../kohsuke/github/GHCommitFileIterable.java  |  83 ++---
 .../org/kohsuke/github/GHCommitFilesPage.java |   9 +-
 .../kohsuke/github/GHCommitSearchBuilder.java |  22 +-
 .../java/org/kohsuke/github/GHCompare.java    |  91 +-----
 .../github/GHContentSearchBuilder.java        |   2 +-
 .../github/GHExternalGroupIterable.java       |  76 ++---
 .../kohsuke/github/GHExternalGroupPage.java   |   6 +-
 .../kohsuke/github/GHIssueSearchBuilder.java  |   4 +-
 .../kohsuke/github/GHNotificationStream.java  |   3 +-
 .../github/GHPullRequestSearchBuilder.java    |   2 +-
 .../github/GHRepositorySearchBuilder.java     |   4 +-
 .../org/kohsuke/github/GHSearchBuilder.java   |  18 +-
 .../kohsuke/github/GHUserSearchBuilder.java   |   2 +-
 .../github/GHWorkflowJobsIterable.java        |  49 +--
 .../kohsuke/github/GHWorkflowJobsPage.java    |  21 +-
 .../github/GHWorkflowRunsIterable.java        |  54 +---
 .../kohsuke/github/GHWorkflowRunsPage.java    |  21 +-
 .../kohsuke/github/GHWorkflowsIterable.java   |  56 +---
 .../org/kohsuke/github/GHWorkflowsPage.java   |  21 +-
 .../github/GitHubEndpointIterable.java        | 292 ++++++++++++++++++
 .../github/GitHubEndpointPageIterator.java    | 135 ++++++++
 .../java/org/kohsuke/github/GitHubPage.java   |  16 +
 .../github/GitHubPageContentsIterable.java    |  93 ------
 .../github/GitHubPageItemIterator.java        | 138 +++++++++
 .../kohsuke/github/GitHubPageIterator.java    | 162 +++-------
 .../org/kohsuke/github/GitHubResponse.java    |   2 +-
 .../org/kohsuke/github/PagedIterable.java     | 138 +--------
 .../org/kohsuke/github/PagedIterator.java     | 128 +-------
 .../kohsuke/github/PagedSearchIterable.java   |  92 +-----
 .../java/org/kohsuke/github/Requester.java    |  32 +-
 .../java/org/kohsuke/github/SearchResult.java |   6 +-
 .../no-reflect-and-serialization-list         |   2 +-
 41 files changed, 867 insertions(+), 1124 deletions(-)
 create mode 100644 src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
 create mode 100644 src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
 create mode 100644 src/main/java/org/kohsuke/github/GitHubPage.java
 delete mode 100644 src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java
 create mode 100644 src/main/java/org/kohsuke/github/GitHubPageItemIterator.java

diff --git a/src/main/java/org/kohsuke/github/GHAppInstallation.java b/src/main/java/org/kohsuke/github/GHAppInstallation.java
index e92c744e99..ec343924e1 100644
--- a/src/main/java/org/kohsuke/github/GHAppInstallation.java
+++ b/src/main/java/org/kohsuke/github/GHAppInstallation.java
@@ -267,6 +267,7 @@ public PagedSearchIterable<GHRepository> listRepositories() {
 
         request = root().createRequest().withUrlPath("/installation/repositories").build();
 
-        return new PagedSearchIterable<>(root(), request, GHAppInstallationRepositoryResult.class);
+        return new PagedSearchIterable<>(new GitHubEndpointIterable<>(root()
+                .getClient(), request, GHAppInstallationRepositoryResult.class, GHRepository.class, null));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java b/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java
index 44ace753a2..a358435008 100644
--- a/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java
+++ b/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java
@@ -10,7 +10,6 @@
  */
 public class GHAppInstallationRequest extends GHObject {
     private GHOrganization account;
-
     private GHUser requester;
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java b/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
index fc89d371ee..d089e24e49 100644
--- a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
@@ -1,9 +1,5 @@
 package org.kohsuke.github;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * Iterable for GHAppInstallation listing.
@@ -12,8 +8,6 @@ class GHAppInstallationsIterable extends PagedIterable<GHAppInstallation> {
 
     /** The Constant APP_INSTALLATIONS_URL. */
     public static final String APP_INSTALLATIONS_URL = "/user/installations";
-    private GHAppInstallationsPage result;
-    private final transient GitHub root;
 
     /**
      * Instantiates a new GH app installations iterable.
@@ -22,45 +16,10 @@ class GHAppInstallationsIterable extends PagedIterable<GHAppInstallation> {
      *            the root
      */
     public GHAppInstallationsIterable(GitHub root) {
-        this.root = root;
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHAppInstallation> _iterator(int pageSize) {
-        final GitHubRequest request = root.createRequest().withUrlPath(APP_INSTALLATIONS_URL).build();
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator.create(root.getClient(), GHAppInstallationsPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<GHAppInstallation[]> adapt(final Iterator<GHAppInstallationsPage> base) {
-        return new Iterator<GHAppInstallation[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public GHAppInstallation[] next() {
-                GHAppInstallationsPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                return v.getInstallations();
-            }
-        };
+        super(new GitHubEndpointIterable<>(root.getClient(),
+                root.createRequest().withUrlPath(APP_INSTALLATIONS_URL).build(),
+                GHAppInstallationsPage.class,
+                GHAppInstallation.class,
+                null));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java b/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java
index cd8f9a1f7e..0c593662d1 100644
--- a/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java
+++ b/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java
@@ -4,10 +4,15 @@
 /**
  * Represents the one page of GHAppInstallations.
  */
-class GHAppInstallationsPage {
+class GHAppInstallationsPage implements GitHubPage<GHAppInstallation> {
     private GHAppInstallation[] installations;
     private int totalCount;
 
+    @Override
+    public GHAppInstallation[] getItems() {
+        return getInstallations();
+    }
+
     /**
      * Gets the total count.
      *
diff --git a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java b/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
index 2a574150cc..b16678ac28 100644
--- a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
@@ -1,18 +1,10 @@
 package org.kohsuke.github;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * Iterable for artifacts listing.
  */
 class GHArtifactsIterable extends PagedIterable<GHArtifact> {
-    private final transient GHRepository owner;
-    private final GitHubRequest request;
-
-    private GHArtifactsPage result;
 
     /**
      * Instantiates a new GH artifacts iterable.
@@ -23,45 +15,10 @@ class GHArtifactsIterable extends PagedIterable<GHArtifact> {
      *            the request builder
      */
     public GHArtifactsIterable(GHRepository owner, GitHubRequest.Builder<?> requestBuilder) {
-        this.owner = owner;
-        this.request = requestBuilder.build();
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHArtifact> _iterator(int pageSize) {
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator.create(owner.root().getClient(), GHArtifactsPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<GHArtifact[]> adapt(final Iterator<GHArtifactsPage> base) {
-        return new Iterator<GHArtifact[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public GHArtifact[] next() {
-                GHArtifactsPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                return v.getArtifacts(owner);
-            }
-        };
+        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+                requestBuilder.build(),
+                GHArtifactsPage.class,
+                GHArtifact.class,
+                item -> item.wrapUp(owner)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHArtifactsPage.java b/src/main/java/org/kohsuke/github/GHArtifactsPage.java
index 8b3675bb11..c4edc38515 100644
--- a/src/main/java/org/kohsuke/github/GHArtifactsPage.java
+++ b/src/main/java/org/kohsuke/github/GHArtifactsPage.java
@@ -8,10 +8,15 @@
  */
 @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" },
         justification = "JSON API")
-class GHArtifactsPage {
+class GHArtifactsPage implements GitHubPage<GHArtifact> {
     private GHArtifact[] artifacts;
     private int totalCount;
 
+    @Override
+    public GHArtifact[] getItems() {
+        return artifacts;
+    }
+
     /**
      * Gets the total count.
      *
@@ -20,18 +25,4 @@ class GHArtifactsPage {
     public int getTotalCount() {
         return totalCount;
     }
-
-    /**
-     * Gets the artifacts.
-     *
-     * @param owner
-     *            the owner
-     * @return the artifacts
-     */
-    GHArtifact[] getArtifacts(GHRepository owner) {
-        for (GHArtifact artifact : artifacts) {
-            artifact.wrapUp(owner);
-        }
-        return artifacts;
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java b/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java
index 73d55ba4c1..875b285287 100644
--- a/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java
+++ b/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java
@@ -39,7 +39,8 @@ public PagedSearchIterable<GHRepository> listRepositories() {
 
         request = root().createRequest().withUrlPath("/installation/repositories").build();
 
-        return new PagedSearchIterable<>(root(), request, GHAuthenticatedAppInstallationRepositoryResult.class);
+        return new PagedSearchIterable<>(new GitHubEndpointIterable<>(root()
+                .getClient(), request, GHAuthenticatedAppInstallationRepositoryResult.class, GHRepository.class, null));
     }
 
 }
diff --git a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java b/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
index 0866bd1f58..5f75eccc4e 100644
--- a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
@@ -1,19 +1,10 @@
 package org.kohsuke.github;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * Iterable for check-runs listing.
  */
 class GHCheckRunsIterable extends PagedIterable<GHCheckRun> {
-    private final GHRepository owner;
-    private final GitHubRequest request;
-
-    private GHCheckRunsPage result;
-
     /**
      * Instantiates a new GH check runs iterable.
      *
@@ -23,45 +14,7 @@ class GHCheckRunsIterable extends PagedIterable<GHCheckRun> {
      *            the request
      */
     public GHCheckRunsIterable(GHRepository owner, GitHubRequest request) {
-        this.owner = owner;
-        this.request = request;
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHCheckRun> _iterator(int pageSize) {
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator.create(owner.root().getClient(), GHCheckRunsPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<GHCheckRun[]> adapt(final Iterator<GHCheckRunsPage> base) {
-        return new Iterator<GHCheckRun[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public GHCheckRun[] next() {
-                GHCheckRunsPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                return v.getCheckRuns(owner);
-            }
-        };
+        super(new GitHubEndpointIterable<>(owner.root()
+                .getClient(), request, GHCheckRunsPage.class, GHCheckRun.class, item -> item.wrap(owner)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHCheckRunsPage.java b/src/main/java/org/kohsuke/github/GHCheckRunsPage.java
index d0b5d012f2..4f36d526c6 100644
--- a/src/main/java/org/kohsuke/github/GHCheckRunsPage.java
+++ b/src/main/java/org/kohsuke/github/GHCheckRunsPage.java
@@ -8,10 +8,15 @@
  */
 @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" },
         justification = "JSON API")
-class GHCheckRunsPage {
+class GHCheckRunsPage implements GitHubPage<GHCheckRun> {
     private GHCheckRun[] checkRuns;
     private int totalCount;
 
+    @Override
+    public GHCheckRun[] getItems() {
+        return checkRuns;
+    }
+
     /**
      * Gets the total count.
      *
@@ -20,18 +25,4 @@ class GHCheckRunsPage {
     public int getTotalCount() {
         return totalCount;
     }
-
-    /**
-     * Gets the check runs.
-     *
-     * @param owner
-     *            the owner
-     * @return the check runs
-     */
-    GHCheckRun[] getCheckRuns(GHRepository owner) {
-        for (GHCheckRun checkRun : checkRuns) {
-            checkRun.wrap(owner);
-        }
-        return checkRuns;
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/GHCommitFileIterable.java b/src/main/java/org/kohsuke/github/GHCommitFileIterable.java
index 808f036017..1d83abc4a1 100644
--- a/src/main/java/org/kohsuke/github/GHCommitFileIterable.java
+++ b/src/main/java/org/kohsuke/github/GHCommitFileIterable.java
@@ -2,12 +2,8 @@
 
 import org.kohsuke.github.GHCommit.File;
 
-import java.util.Collections;
-import java.util.Iterator;
 import java.util.List;
 
-import javax.annotation.Nonnull;
-
 /**
  * Iterable for commit listing.
  *
@@ -21,76 +17,41 @@ class GHCommitFileIterable extends PagedIterable<GHCommit.File> {
      */
     private static final int GH_FILE_LIMIT_PER_COMMIT_PAGE = 300;
 
-    private final File[] files;
-    private final GHRepository owner;
-    private final String sha;
-
-    /**
-     * Instantiates a new GH commit iterable.
-     *
-     * @param owner
-     *            the owner
-     * @param sha
-     *            the SHA of the commit
-     * @param files
-     *            the list of files initially populated
-     */
-    public GHCommitFileIterable(GHRepository owner, String sha, List<File> files) {
-        this.owner = owner;
-        this.sha = sha;
-        this.files = files != null ? files.toArray(new File[0]) : null;
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHCommit.File> _iterator(int pageSize) {
-
-        Iterator<GHCommit.File[]> pageIterator;
-
+    private static GitHubEndpointIterable<GHCommitFilesPage, File> createEndpointIterable(GHRepository owner,
+            String sha,
+            GHCommit.File[] files) {
+        GitHubEndpointIterable<GHCommitFilesPage, File> iterable;
         if (files != null && files.length < GH_FILE_LIMIT_PER_COMMIT_PAGE) {
             // create a page iterator that only provides one page
-            pageIterator = Collections.singleton(files).iterator();
+            iterable = GitHubEndpointIterable.ofSingleton(new GHCommitFilesPage(files));
         } else {
-            // page size is controlled by the server for this iterator, do not allow it to be set by the caller
-            pageSize = 0;
-
             GitHubRequest request = owner.root()
                     .createRequest()
                     .withUrlPath(owner.getApiTailUrl("commits/" + sha))
                     .build();
-
-            pageIterator = adapt(
-                    GitHubPageIterator.create(owner.root().getClient(), GHCommitFilesPage.class, request, pageSize));
+            iterable = new GitHubEndpointIterable<>(owner.root()
+                    .getClient(), request, GHCommitFilesPage.class, GHCommit.File.class, null);
         }
-
-        return new PagedIterator<>(pageIterator, null);
+        return iterable;
     }
 
     /**
-     * Adapt.
+     * Instantiates a new GH commit iterable.
      *
-     * @param base
-     *            the base commit page
-     * @return the iterator
+     * @param owner
+     *            the owner
+     * @param sha
+     *            the SHA of the commit
+     * @param files
+     *            the list of files initially populated
      */
-    protected Iterator<GHCommit.File[]> adapt(final Iterator<GHCommitFilesPage> base) {
-        return new Iterator<GHCommit.File[]>() {
-
-            public boolean hasNext() {
-                return base.hasNext();
-            }
+    public GHCommitFileIterable(GHRepository owner, String sha, List<GHCommit.File> files) {
+        super(createEndpointIterable(owner, sha, files != null ? files.toArray(new File[0]) : null));
+    }
 
-            public GHCommit.File[] next() {
-                GHCommitFilesPage v = base.next();
-                return v.getFiles();
-            }
-        };
+    @Override
+    public PagedIterable<File> withPageSize(int i) {
+        // page size is controlled by the server for this iterable, do not allow it to be set by the caller
+        return this;
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHCommitFilesPage.java b/src/main/java/org/kohsuke/github/GHCommitFilesPage.java
index d2ab10dc98..c23761ae27 100644
--- a/src/main/java/org/kohsuke/github/GHCommitFilesPage.java
+++ b/src/main/java/org/kohsuke/github/GHCommitFilesPage.java
@@ -7,24 +7,23 @@
  *
  * @author Stephen Horgan
  */
-class GHCommitFilesPage {
+class GHCommitFilesPage implements GitHubPage<GHCommit.File> {
     private File[] files;
 
     public GHCommitFilesPage() {
     }
 
-    public GHCommitFilesPage(File[] files) {
+    GHCommitFilesPage(File[] files) {
         this.files = files;
     }
 
     /**
      * Gets the files.
      *
-     * @param owner
-     *            the owner
      * @return the files
      */
-    File[] getFiles() {
+    @Override
+    public File[] getItems() {
         return files;
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java
index 3a1ddbffce..ecc1042df5 100644
--- a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java
@@ -33,14 +33,6 @@ private static class CommitSearchResult extends SearchResult<GHCommit> {
 
         @Override
         GHCommit[] getItems(GitHub root) {
-            for (GHCommit commit : items) {
-                String repoName = getRepoName(commit.url);
-                try {
-                    GHRepository repo = root.getRepository(repoName);
-                    commit.wrapUp(repo);
-                } catch (IOException ioe) {
-                }
-            }
             return items;
         }
     }
@@ -66,7 +58,7 @@ private static String getRepoName(String commitUrl) {
      *            the root
      */
     GHCommitSearchBuilder(GitHub root) {
-        super(root, CommitSearchResult.class);
+        super(root, CommitSearchResult.class, GHCommit.class);
     }
 
     /**
@@ -179,6 +171,18 @@ public GHCommitSearchBuilder is(String v) {
         return q("is:" + v);
     }
 
+    @Override
+    public PagedSearchIterable<GHCommit> list() {
+        return list(item -> {
+            String repoName = getRepoName(item.url);
+            try {
+                GHRepository repo = root().getRepository(repoName);
+                item.wrapUp(repo);
+            } catch (IOException ioe) {
+            }
+        });
+    }
+
     /**
      * Merge gh commit search builder.
      *
diff --git a/src/main/java/org/kohsuke/github/GHCompare.java b/src/main/java/org/kohsuke/github/GHCompare.java
index 48340fda36..52ccb8a922 100644
--- a/src/main/java/org/kohsuke/github/GHCompare.java
+++ b/src/main/java/org/kohsuke/github/GHCompare.java
@@ -5,10 +5,6 @@
 
 import java.io.IOException;
 import java.net.URL;
-import java.util.Collections;
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
 
 // TODO: Auto-generated Javadoc
 /**
@@ -16,7 +12,7 @@
  *
  * @author Michael Clarke
  */
-public class GHCompare {
+public class GHCompare implements GitHubPage<GHCompare.Commit> {
 
     /**
      * Compare commits had a child commit element with additional details we want to capture. This extension of GHCommit
@@ -113,6 +109,7 @@ public String getUrl() {
             return url;
         }
     }
+
     /**
      * The enum Status.
      */
@@ -158,70 +155,9 @@ public String getUrl() {
             return url;
         }
     }
-    /**
-     * Iterable for commit listing.
-     */
-    class GHCompareCommitsIterable extends PagedIterable<Commit> {
-
-        private GHCompare result;
-
-        /**
-         * Instantiates a new GH compare commits iterable.
-         */
-        public GHCompareCommitsIterable() {
-        }
-
-        /**
-         * Iterator.
-         *
-         * @param pageSize
-         *            the page size
-         * @return the paged iterator
-         */
-        @Nonnull
-        @Override
-        public PagedIterator<Commit> _iterator(int pageSize) {
-            GitHubRequest request = owner.root()
-                    .createRequest()
-                    .injectMappingValue("GHCompare_usePaginatedCommits", usePaginatedCommits)
-                    .withUrlPath(owner.getApiTailUrl(url.substring(url.lastIndexOf("/compare/"))))
-                    .build();
-
-            // page_size must be set for GHCompare commit pagination
-            if (pageSize == 0) {
-                pageSize = 10;
-            }
-            return new PagedIterator<>(
-                    adapt(GitHubPageIterator.create(owner.root().getClient(), GHCompare.class, request, pageSize)),
-                    item -> item.wrapUp(owner));
-        }
-
-        /**
-         * Adapt.
-         *
-         * @param base
-         *            the base
-         * @return the iterator
-         */
-        protected Iterator<Commit[]> adapt(final Iterator<GHCompare> base) {
-            return new Iterator<Commit[]>() {
-                public boolean hasNext() {
-                    return base.hasNext();
-                }
-
-                public Commit[] next() {
-                    GHCompare v = base.next();
-                    if (result == null) {
-                        result = v;
-                    }
-                    return v.commits;
-                }
-            };
-        }
-    }
     private int aheadBy, behindBy, totalCommits;
-    private Commit baseCommit, mergeBaseCommit;
 
+    private Commit baseCommit, mergeBaseCommit;
     private Commit[] commits;
 
     private GHCommit.File[] files;
@@ -324,6 +260,11 @@ public URL getHtmlUrl() {
         return GitHubClient.parseURL(htmlUrl);
     }
 
+    @Override
+    public GHCompare.Commit[] getItems() {
+        return commits;
+    }
+
     /**
      * Gets merge base commit.
      *
@@ -395,16 +336,16 @@ public URL getUrl() {
      */
     public PagedIterable<Commit> listCommits() {
         if (usePaginatedCommits) {
-            return new GHCompareCommitsIterable();
+            final GHRepository owner = this.owner;
+            return owner.root()
+                    .createRequest()
+                    .injectMappingValue("GHCompare_usePaginatedCommits", usePaginatedCommits)
+                    .withUrlPath(owner.getApiTailUrl(url.substring(url.lastIndexOf("/compare/"))))
+                    .toIterable(GHCompare.class, Commit.class, item -> item.wrapUp(owner))
+                    .withPageSize(10);
         } else {
             // if not using paginated commits, adapt the returned commits array
-            return new PagedIterable<Commit>() {
-                @Nonnull
-                @Override
-                public PagedIterator<Commit> _iterator(int pageSize) {
-                    return new PagedIterator<>(Collections.singleton(commits).iterator(), null);
-                }
-            };
+            return new PagedIterable<>(GitHubEndpointIterable.ofSingleton(this.commits));
         }
     }
 
diff --git a/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java b/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java
index bdd16cea3e..7aa1243f38 100644
--- a/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java
@@ -36,7 +36,7 @@ GHContent[] getItems(GitHub root) {
      *            the root
      */
     GHContentSearchBuilder(GitHub root) {
-        super(root, ContentSearchResult.class);
+        super(root, ContentSearchResult.class, GHContent.class);
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java b/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java
index f921fdd920..a4a032bab7 100644
--- a/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java
+++ b/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java
@@ -1,9 +1,6 @@
 package org.kohsuke.github;
 
-import java.util.Arrays;
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
+import org.jetbrains.annotations.NotNull;
 
 /**
  * Iterable for external group listing.
@@ -12,12 +9,6 @@
  */
 class GHExternalGroupIterable extends PagedIterable<GHExternalGroup> {
 
-    private final GHOrganization owner;
-
-    private final GitHubRequest request;
-
-    private GHExternalGroupPage result;
-
     /**
      * Instantiates a new GH external groups iterable.
      *
@@ -26,52 +17,25 @@ class GHExternalGroupIterable extends PagedIterable<GHExternalGroup> {
      * @param requestBuilder
      *            the request builder
      */
-    GHExternalGroupIterable(final GHOrganization owner, final GitHubRequest.Builder<?> requestBuilder) {
-        this.owner = owner;
-        this.request = requestBuilder.build();
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHExternalGroup> _iterator(int pageSize) {
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator
-                        .create(owner.root().getClient(), GHExternalGroupPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    private Iterator<GHExternalGroup[]> adapt(final Iterator<GHExternalGroupPage> base) {
-        return new Iterator<GHExternalGroup[]>() {
-            public boolean hasNext() {
-                try {
-                    return base.hasNext();
-                } catch (final GHException e) {
-                    throw EnterpriseManagedSupport.forOrganization(owner).filterException(e).orElse(e);
-                }
-            }
-
-            public GHExternalGroup[] next() {
-                GHExternalGroupPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                Arrays.stream(v.getGroups()).forEach(g -> g.wrapUp(owner));
-                return v.getGroups();
+    GHExternalGroupIterable(final GHOrganization owner, GitHubRequest.Builder<?> requestBuilder) {
+        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+                requestBuilder.build(),
+                GHExternalGroupPage.class,
+                GHExternalGroup.class,
+                item -> item.wrapUp(owner)) {
+            @NotNull @Override
+            public GitHubEndpointPageIterator<GHExternalGroupPage, GHExternalGroup> pageIterator() {
+                return new GitHubEndpointPageIterator<>(client, pageType, request, pageSize, itemInitializer) {
+                    @Override
+                    public boolean hasNext() {
+                        try {
+                            return super.hasNext();
+                        } catch (final GHException e) {
+                            throw EnterpriseManagedSupport.forOrganization(owner).filterException(e).orElse(e);
+                        }
+                    }
+                };
             }
-        };
+        });
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHExternalGroupPage.java b/src/main/java/org/kohsuke/github/GHExternalGroupPage.java
index d47b49678c..02441fcfff 100644
--- a/src/main/java/org/kohsuke/github/GHExternalGroupPage.java
+++ b/src/main/java/org/kohsuke/github/GHExternalGroupPage.java
@@ -7,7 +7,7 @@
  *
  * @author Miguel Esteban GutiƩrrez
  */
-class GHExternalGroupPage {
+class GHExternalGroupPage implements GitHubPage<GHExternalGroup> {
 
     private static final GHExternalGroup[] GH_EXTERNAL_GROUPS = new GHExternalGroup[0];
 
@@ -31,4 +31,8 @@ public GHExternalGroup[] getGroups() {
         return groups;
     }
 
+    @Override
+    public GHExternalGroup[] getItems() {
+        return getGroups();
+    }
 }
diff --git a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java
index 3967691ac3..507a254864 100644
--- a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java
@@ -32,8 +32,6 @@ private static class IssueSearchResult extends SearchResult<GHIssue> {
 
         @Override
         GHIssue[] getItems(GitHub root) {
-            for (GHIssue i : items) {
-            }
             return items;
         }
     }
@@ -45,7 +43,7 @@ GHIssue[] getItems(GitHub root) {
      *            the root
      */
     GHIssueSearchBuilder(GitHub root) {
-        super(root, IssueSearchResult.class);
+        super(root, IssueSearchResult.class, GHIssue.class);
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHNotificationStream.java b/src/main/java/org/kohsuke/github/GHNotificationStream.java
index 269ddf972f..28e075b812 100644
--- a/src/main/java/org/kohsuke/github/GHNotificationStream.java
+++ b/src/main/java/org/kohsuke/github/GHNotificationStream.java
@@ -143,8 +143,7 @@ GHThread fetch() {
                         }
 
                         Requester requester = req.withUrlPath(apiUrl);
-                        GitHubResponse<GHThread[]> response = ((GitHubPageContentsIterable<GHThread>) requester
-                                .toIterable(GHThread[].class, null)).toResponse();
+                        GitHubResponse<GHThread[]> response = requester.toIterable(GHThread[].class, null).toResponse();
                         threads = response.body();
 
                         if (threads == null) {
diff --git a/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java b/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java
index 143f6e6ae2..910b8c6163 100644
--- a/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java
@@ -44,7 +44,7 @@ GHPullRequest[] getItems(GitHub root) {
      *            the root
      */
     GHPullRequestSearchBuilder(GitHub root) {
-        super(root, PullRequestSearchResult.class);
+        super(root, PullRequestSearchResult.class, GHPullRequest.class);
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java
index 9e600ec927..c0d0896658 100644
--- a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java
@@ -32,8 +32,6 @@ private static class RepositorySearchResult extends SearchResult<GHRepository> {
 
         @Override
         GHRepository[] getItems(GitHub root) {
-            for (GHRepository item : items) {
-            }
             return items;
         }
     }
@@ -45,7 +43,7 @@ GHRepository[] getItems(GitHub root) {
      *            the root
      */
     GHRepositorySearchBuilder(GitHub root) {
-        super(root, RepositorySearchResult.class);
+        super(root, RepositorySearchResult.class, GHRepository.class);
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHSearchBuilder.java b/src/main/java/org/kohsuke/github/GHSearchBuilder.java
index ea6317426c..3cabb7800c 100644
--- a/src/main/java/org/kohsuke/github/GHSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHSearchBuilder.java
@@ -4,6 +4,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Consumer;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nonnull;
@@ -18,11 +19,12 @@
  */
 public abstract class GHSearchBuilder<T> extends GHQueryBuilder<T> {
 
+    private final Class<T> itemType;
+
     /**
      * Data transfer object that receives the result of search.
      */
     private final Class<? extends SearchResult<T>> receiverType;
-
     /** The terms. */
     protected final List<String> terms = new ArrayList<String>();
 
@@ -34,9 +36,10 @@ public abstract class GHSearchBuilder<T> extends GHQueryBuilder<T> {
      * @param receiverType
      *            the receiver type
      */
-    GHSearchBuilder(GitHub root, Class<? extends SearchResult<T>> receiverType) {
+    GHSearchBuilder(GitHub root, Class<? extends SearchResult<T>> receiverType, Class<T> itemType) {
         super(root);
         this.receiverType = receiverType;
+        this.itemType = itemType;
         req.withUrlPath(getApiUrl());
         req.rateLimit(RateLimitTarget.SEARCH);
     }
@@ -48,9 +51,7 @@ public abstract class GHSearchBuilder<T> extends GHQueryBuilder<T> {
      */
     @Override
     public PagedSearchIterable<T> list() {
-
-        req.set("q", StringUtils.join(terms, " "));
-        return new PagedSearchIterable<>(root(), req.build(), receiverType);
+        return list(null);
     }
 
     /**
@@ -72,6 +73,13 @@ public GHQueryBuilder<T> q(String term) {
      */
     protected abstract String getApiUrl();
 
+    PagedSearchIterable<T> list(Consumer<T> itemInitializer) {
+
+        req.set("q", StringUtils.join(terms, " "));
+        return new PagedSearchIterable<>(
+                new GitHubEndpointIterable<>(root().getClient(), req.build(), receiverType, itemType, itemInitializer));
+    }
+
     /**
      * Add a search term with qualifier.
      *
diff --git a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
index 0193b2139e..6852114b26 100644
--- a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java
@@ -38,7 +38,7 @@ GHUser[] getItems(GitHub root) {
      *            the root
      */
     GHUserSearchBuilder(GitHub root) {
-        super(root, UserSearchResult.class);
+        super(root, UserSearchResult.class, GHUser.class);
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
index 6ab751850d..38fe89f524 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
@@ -1,17 +1,10 @@
 package org.kohsuke.github;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * Iterable for workflow run jobs listing.
  */
 class GHWorkflowJobsIterable extends PagedIterable<GHWorkflowJob> {
-    private final GHRepository repo;
-    private final GitHubRequest request;
-
     private GHWorkflowJobsPage result;
 
     /**
@@ -23,45 +16,7 @@ class GHWorkflowJobsIterable extends PagedIterable<GHWorkflowJob> {
      *            the request
      */
     public GHWorkflowJobsIterable(GHRepository repo, GitHubRequest request) {
-        this.repo = repo;
-        this.request = request;
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHWorkflowJob> _iterator(int pageSize) {
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator.create(repo.root().getClient(), GHWorkflowJobsPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<GHWorkflowJob[]> adapt(final Iterator<GHWorkflowJobsPage> base) {
-        return new Iterator<GHWorkflowJob[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public GHWorkflowJob[] next() {
-                GHWorkflowJobsPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                return v.getWorkflowJobs(repo);
-            }
-        };
+        super(new GitHubEndpointIterable<>(repo.root()
+                .getClient(), request, GHWorkflowJobsPage.class, GHWorkflowJob.class, item -> item.wrapUp(repo)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java b/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java
index 8d4a7ca772..02d45de338 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java
@@ -8,10 +8,15 @@
  */
 @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" },
         justification = "JSON API")
-class GHWorkflowJobsPage {
+class GHWorkflowJobsPage implements GitHubPage<GHWorkflowJob> {
     private GHWorkflowJob[] jobs;
     private int totalCount;
 
+    @Override
+    public GHWorkflowJob[] getItems() {
+        return jobs;
+    }
+
     /**
      * Gets the total count.
      *
@@ -20,18 +25,4 @@ class GHWorkflowJobsPage {
     public int getTotalCount() {
         return totalCount;
     }
-
-    /**
-     * Gets the workflow jobs.
-     *
-     * @param repo
-     *            the repo
-     * @return the workflow jobs
-     */
-    GHWorkflowJob[] getWorkflowJobs(GHRepository repo) {
-        for (GHWorkflowJob job : jobs) {
-            job.wrapUp(repo);
-        }
-        return jobs;
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
index 4a525a83dc..532d3e6097 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
@@ -1,19 +1,10 @@
 package org.kohsuke.github;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * Iterable for workflow runs listing.
  */
 class GHWorkflowRunsIterable extends PagedIterable<GHWorkflowRun> {
-    private final GHRepository owner;
-    private final GitHubRequest request;
-
-    private GHWorkflowRunsPage result;
-
     /**
      * Instantiates a new GH workflow runs iterable.
      *
@@ -23,45 +14,10 @@ class GHWorkflowRunsIterable extends PagedIterable<GHWorkflowRun> {
      *            the request builder
      */
     public GHWorkflowRunsIterable(GHRepository owner, GitHubRequest.Builder<?> requestBuilder) {
-        this.owner = owner;
-        this.request = requestBuilder.build();
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHWorkflowRun> _iterator(int pageSize) {
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator.create(owner.root().getClient(), GHWorkflowRunsPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<GHWorkflowRun[]> adapt(final Iterator<GHWorkflowRunsPage> base) {
-        return new Iterator<GHWorkflowRun[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public GHWorkflowRun[] next() {
-                GHWorkflowRunsPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                return v.getWorkflowRuns(owner);
-            }
-        };
+        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+                requestBuilder.build(),
+                GHWorkflowRunsPage.class,
+                GHWorkflowRun.class,
+                item -> item.wrapUp(owner)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java b/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java
index 8df067dead..5e0ebaca99 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java
@@ -8,10 +8,15 @@
  */
 @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" },
         justification = "JSON API")
-class GHWorkflowRunsPage {
+class GHWorkflowRunsPage implements GitHubPage<GHWorkflowRun> {
     private int totalCount;
     private GHWorkflowRun[] workflowRuns;
 
+    @Override
+    public GHWorkflowRun[] getItems() {
+        return workflowRuns;
+    }
+
     /**
      * Gets the total count.
      *
@@ -20,18 +25,4 @@ class GHWorkflowRunsPage {
     public int getTotalCount() {
         return totalCount;
     }
-
-    /**
-     * Gets the workflow runs.
-     *
-     * @param owner
-     *            the owner
-     * @return the workflow runs
-     */
-    GHWorkflowRun[] getWorkflowRuns(GHRepository owner) {
-        for (GHWorkflowRun workflowRun : workflowRuns) {
-            workflowRun.wrapUp(owner);
-        }
-        return workflowRuns;
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
index 66d3d9480f..265d1700f5 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
@@ -1,17 +1,10 @@
 package org.kohsuke.github;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * Iterable for workflows listing.
  */
 class GHWorkflowsIterable extends PagedIterable<GHWorkflow> {
-    private final transient GHRepository owner;
-
-    private GHWorkflowsPage result;
 
     /**
      * Instantiates a new GH workflows iterable.
@@ -20,49 +13,10 @@ class GHWorkflowsIterable extends PagedIterable<GHWorkflow> {
      *            the owner
      */
     public GHWorkflowsIterable(GHRepository owner) {
-        this.owner = owner;
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<GHWorkflow> _iterator(int pageSize) {
-        GitHubRequest request = owner.root()
-                .createRequest()
-                .withUrlPath(owner.getApiTailUrl("actions/workflows"))
-                .build();
-
-        return new PagedIterator<>(
-                adapt(GitHubPageIterator.create(owner.root().getClient(), GHWorkflowsPage.class, request, pageSize)),
-                null);
-    }
-
-    /**
-     * Adapt.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<GHWorkflow[]> adapt(final Iterator<GHWorkflowsPage> base) {
-        return new Iterator<GHWorkflow[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public GHWorkflow[] next() {
-                GHWorkflowsPage v = base.next();
-                if (result == null) {
-                    result = v;
-                }
-                return v.getWorkflows(owner);
-            }
-        };
+        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+                owner.root().createRequest().withUrlPath(owner.getApiTailUrl("actions/workflows")).build(),
+                GHWorkflowsPage.class,
+                GHWorkflow.class,
+                item -> item.wrapUp(owner)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowsPage.java b/src/main/java/org/kohsuke/github/GHWorkflowsPage.java
index 1fb87a8147..134bdd6419 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowsPage.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowsPage.java
@@ -8,10 +8,15 @@
  */
 @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" },
         justification = "JSON API")
-class GHWorkflowsPage {
+class GHWorkflowsPage implements GitHubPage<GHWorkflow> {
     private int totalCount;
     private GHWorkflow[] workflows;
 
+    @Override
+    public GHWorkflow[] getItems() {
+        return workflows;
+    }
+
     /**
      * Gets the total count.
      *
@@ -20,18 +25,4 @@ class GHWorkflowsPage {
     public int getTotalCount() {
         return totalCount;
     }
-
-    /**
-     * Gets the workflows.
-     *
-     * @param owner
-     *            the owner
-     * @return the workflows
-     */
-    GHWorkflow[] getWorkflows(GHRepository owner) {
-        for (GHWorkflow workflow : workflows) {
-            workflow.wrapUp(owner);
-        }
-        return workflows;
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java b/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
new file mode 100644
index 0000000000..804a497a1d
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
@@ -0,0 +1,292 @@
+package org.kohsuke.github;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.lang.reflect.Array;
+import java.util.*;
+import java.util.function.Consumer;
+
+import javax.annotation.Nonnull;
+
+/**
+ * {@link GitHubEndpointIterable} implementation that take a {@link Consumer} that initializes all the items on each
+ * page as they are retrieved.
+ *
+ * {@link GitHubEndpointIterable} is immutable and thread-safe, but the iterator returned from {@link #iterator()} is
+ * not. Any one instance of iterator should only be called from a single thread.
+ *
+ * @author Liam Newman
+ * @param <Item>
+ *            the type of items on each page
+ */
+class GitHubEndpointIterable<Page extends GitHubPage<Item>, Item> implements Iterable<Item> {
+
+    private static class ArrayIterable<I> extends GitHubEndpointIterable<GitHubPage<I>, I> {
+
+        private class ArrayIterator extends GitHubEndpointPageIterator<GitHubPage<I>, I> {
+
+            ArrayIterator(GitHubClient client,
+                    Class<GitHubPage<I>> pageType,
+                    GitHubRequest request,
+                    int pageSize,
+                    Consumer<I> itemInitializer) {
+                super(client, pageType, request, pageSize, itemInitializer);
+            }
+
+            @Override
+            @NotNull protected GitHubResponse<GitHubPage<I>> sendNextRequest() throws IOException {
+                GitHubResponse<I[]> response = client.sendRequest(nextRequest,
+                        (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, receiverType));
+                return new GitHubResponse<>(response, new GitHubArrayPage<>(response.body()));
+            }
+
+        }
+
+        private final Class<I[]> receiverType;
+
+        private ArrayIterable(GitHubClient client,
+                GitHubRequest request,
+                Class<I[]> receiverType,
+                Consumer<I> itemInitializer) {
+            super(client,
+                    request,
+                    GitHubArrayPage.getArrayPageClass(receiverType),
+                    (Class<I>) receiverType.getComponentType(),
+                    itemInitializer);
+            this.receiverType = receiverType;
+        }
+
+        @NotNull @Override
+        public GitHubEndpointPageIterator<GitHubPage<I>, I> pageIterator() {
+            return new ArrayIterator(client, pageType, request, pageSize, itemInitializer);
+        }
+    }
+
+    /**
+     * Represents the result of a search.
+     *
+     * @author Kohsuke Kawaguchi
+     * @param <I>
+     *            the generic type
+     */
+    private static class GitHubArrayPage<I> implements GitHubPage<I> {
+
+        private static <P extends GitHubPage<I>, I> Class<P> getArrayPageClass(Class<I[]> receiverType) {
+            return (Class<P>) new GitHubArrayPage<>(receiverType).getClass();
+        }
+
+        private final I[] items;
+
+        public GitHubArrayPage(I[] items) {
+            this.items = items;
+        }
+
+        private GitHubArrayPage(Class<I[]> receiverType) {
+            this.items = (I[]) Array.newInstance(receiverType.getComponentType(), 0);
+        }
+
+        public I[] getItems() {
+            return items;
+        }
+    }
+
+    static <I> GitHubEndpointIterable<GitHubPage<I>, I> ofArrayEndpoint(GitHubClient client,
+            GitHubRequest request,
+            Class<I[]> receiverType,
+            Consumer<I> itemInitializer) {
+        return new ArrayIterable<>(client, request, receiverType, itemInitializer);
+    }
+
+    static <I> GitHubEndpointIterable<GitHubPage<I>, I> ofSingleton(I[] array) {
+        return ofSingleton(new GitHubArrayPage<>(array));
+    }
+
+    static <P extends GitHubPage<I>, I> GitHubEndpointIterable<P, I> ofSingleton(P page) {
+        Class<I> itemType = (Class<I>) page.getItems().getClass().getComponentType();
+        return new GitHubEndpointIterable<>(null, null, (Class<P>) page.getClass(), itemType, null) {
+            @Nonnull
+            @Override
+            public GitHubPageIterator<P, I> pageIterator() {
+                return GitHubPageIterator.ofSingleton(page);
+            }
+        };
+    }
+
+    protected final GitHubClient client;
+    protected final Consumer<Item> itemInitializer;
+    protected final Class<Item> itemType;
+
+    /**
+     * Page size. 0 is default.
+     */
+    protected int pageSize = 0;
+
+    protected final Class<Page> pageType;
+
+    protected final GitHubRequest request;
+
+    /**
+     * Instantiates a new git hub page contents iterable.
+     *
+     * @param client
+     *            the client
+     * @param request
+     *            the request
+     * @param pageType
+     *            the receiver type
+     * @param itemInitializer
+     *            the item initializer
+     */
+    GitHubEndpointIterable(GitHubClient client,
+            GitHubRequest request,
+            Class<Page> pageType,
+            Class<Item> itemType,
+            Consumer<Item> itemInitializer) {
+        this.client = client;
+        this.request = request;
+        this.pageType = pageType;
+        this.itemType = itemType;
+        this.itemInitializer = itemInitializer;
+    }
+
+    @Nonnull
+    public final GitHubPageItemIterator<Page, Item> itemIterator() {
+        return new GitHubPageItemIterator<>(this.pageIterator());
+    }
+    @Nonnull
+    @Override
+    public final Iterator<Item> iterator() {
+        return this.itemIterator();
+    }
+
+    /**
+     *
+     * @return
+     */
+    @Nonnull
+    public GitHubPageIterator<Page, Item> pageIterator() {
+        return new GitHubEndpointPageIterator<>(client, pageType, request, pageSize, itemInitializer);
+    }
+
+    /**
+     * Eagerly walk {@link Iterable} and return the result in an array.
+     *
+     * @return the list
+     * @throws IOException
+     *             if an I/O exception occurs.
+     */
+    @Nonnull
+    public final Item[] toArray() throws IOException {
+        return toArray(pageIterator(), itemType);
+    }
+
+    /**
+     * Eagerly walk {@link Iterable} and return the result in a list.
+     *
+     * @return the list
+     * @throws IOException
+     *             if an I/O Exception occurs
+     */
+    @Nonnull
+    public final List<Item> toList() throws IOException {
+        return Collections.unmodifiableList(Arrays.asList(this.toArray()));
+    }
+
+    /**
+     * Eagerly walk {@link Iterable} and return the result in a set.
+     *
+     * @return the set
+     * @throws IOException
+     *             if an I/O Exception occurs
+     */
+    @Nonnull
+    public final Set<Item> toSet() throws IOException {
+        return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(this.toArray())));
+    }
+
+    /**
+     * Sets the pagination size.
+     *
+     * <p>
+     * When set to non-zero, each API call will retrieve this many entries.
+     *
+     * @param size
+     *            the size
+     * @return the paged iterable
+     */
+    public final GitHubEndpointIterable<Page, Item> withPageSize(int size) {
+        this.pageSize = size;
+        return this;
+    }
+
+    /**
+     * Concatenates a list of arrays into a single array.
+     *
+     * @param pages
+     *            the list of arrays to be concatenated.
+     * @param totalLength
+     *            the total length of the returned array.
+     * @return an array containing all elements from all pages.
+     */
+    @Nonnull
+    private Item[] concatenatePages(List<Item[]> pages, int totalLength) {
+        Item[] result = (Item[]) Array.newInstance(itemType, totalLength);
+
+        int position = 0;
+        for (Item[] page : pages) {
+            final int pageLength = Array.getLength(page);
+            System.arraycopy(page, 0, result, position, pageLength);
+            position += pageLength;
+        }
+        return result;
+    }
+
+    /**
+     * Eagerly walk {@link PagedIterator} and return the result in an array.
+     *
+     * @param iterator
+     *            the {@link PagedIterator} to read
+     * @return an array of all elements from the {@link PagedIterator}
+     * @throws IOException
+     *             if an I/O exception occurs.
+     */
+    private Item[] toArray(final GitHubPageIterator<Page, Item> iterator, Class<Item> itemType) throws IOException {
+        try {
+            ArrayList<Item[]> pages = new ArrayList<>();
+            int totalSize = 0;
+            Item[] item;
+            while (iterator.hasNext()) {
+                item = iterator.next().getItems();
+                totalSize += Array.getLength(item);
+                pages.add(item);
+            }
+
+            return concatenatePages(pages, totalSize);
+        } catch (GHException e) {
+            // if there was an exception inside the iterator it is wrapped as a GHException
+            // if the wrapped exception is an IOException, throw that
+            if (e.getCause() instanceof IOException) {
+                throw (IOException) e.getCause();
+            } else {
+                throw e;
+            }
+        }
+    }
+
+    /**
+     * Eagerly walk {@link Iterable} and return the result in a {@link GitHubResponse} containing an array of {@code T}
+     * items.
+     *
+     * @return the last response with an array containing all the results from all pages.
+     * @throws IOException
+     *             if an I/O exception occurs.
+     */
+    @Nonnull
+    final GitHubResponse<Item[]> toResponse() throws IOException {
+        GitHubPageIterator<Page, Item> iterator = pageIterator();
+        Item[] items = toArray(iterator, itemType);
+        GitHubResponse<Page> lastResponse = iterator.finalResponse();
+        return new GitHubResponse<>(lastResponse, items);
+    }
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java b/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
new file mode 100644
index 0000000000..9440024b77
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
@@ -0,0 +1,135 @@
+package org.kohsuke.github;
+
+import org.jetbrains.annotations.NotNull;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.function.Consumer;
+
+/**
+ * May be used for any item that has pagination information. Iterates over paginated {@code P} objects (not the items
+ * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse}{@code
+ *
+<P>
+ * } after iterating completes.
+ * <p>
+ * Works for array responses, also works for search results which are single instances with an array of items inside.
+ * <p>
+ * This class is not thread-safe. Any one instance should only be called from a single thread.
+ *
+ * @author Liam Newman
+ * @param <P>
+ *            type of each page (not the items in the page).
+ */
+class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> extends GitHubPageIterator<P, Item> {
+
+    /**
+     * When done iterating over pages, it is on rare occasions useful to be able to get information from the final
+     * response that was retrieved.
+     */
+    private GitHubResponse<P> finalResponse = null;
+
+    protected final GitHubClient client;
+
+    /**
+     * The request that will be sent when to get a new response page if {@link #next} is {@code null}. Will be
+     * {@code null} when there are no more pages to fetch.
+     */
+    protected GitHubRequest nextRequest;
+
+    GitHubEndpointPageIterator(GitHubClient client,
+            Class<P> pageType,
+            GitHubRequest request,
+            int pageSize,
+            Consumer<Item> itemInitializer) {
+        super(pageType, itemInitializer);
+
+        if (pageSize > 0) {
+            GitHubRequest.Builder<?> builder = request.toBuilder().with("per_page", pageSize);
+            request = builder.build();
+        }
+
+        if (request != null && !"GET".equals(request.method())) {
+            throw new IllegalArgumentException("Request method \"GET\" is required for page iterator.");
+        }
+
+        this.client = client;
+        this.nextRequest = request;
+    }
+
+    /**
+     * On rare occasions the final response from iterating is needed.
+     *
+     * @return the final response of the iterator.
+     */
+    public GitHubResponse<P> finalResponse() {
+        if (hasNext()) {
+            throw new GHException("Final response is not available until after iterator is done.");
+        }
+        return finalResponse;
+    }
+
+    /**
+     * Locate the next page from the pagination "Link" tag.
+     */
+    private void updateNextRequest(GitHubResponse<P> nextResponse) {
+        GitHubRequest result = null;
+        String link = nextResponse.header("Link");
+        if (link != null) {
+            for (String token : link.split(", ")) {
+                if (token.endsWith("rel=\"next\"")) {
+                    // found the next page. This should look something like
+                    // <https://api.github.com/repos?page=3&per_page=100>; rel="next"
+                    int idx = token.indexOf('>');
+                    result = nextRequest.toBuilder().setRawUrlPath(token.substring(1, idx)).build();
+                    break;
+                }
+            }
+        }
+        nextRequest = result;
+        if (nextRequest == null) {
+            // If this is the last page, keep the response
+            finalResponse = nextResponse;
+        }
+    }
+
+    /**
+     * Fetch is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it is
+     * needed.
+     * <p>
+     * If {@link #next} is not {@code null}, no further action is needed. If {@link #next} is {@code null} and
+     * {@link #nextRequest} is {@code null}, there are no more pages to fetch.
+     * </p>
+     * <p>
+     * Otherwise, a new response page is fetched using {@link #nextRequest}. The response is then checked to see if
+     * there is a page after it and {@link #nextRequest} is updated to point to it. If there are no pages available
+     * after the current response, {@link #nextRequest} is set to {@code null}.
+     * </p>
+     */
+    @Override
+    protected P fetchNext() {
+        if (next != null || nextRequest == null)
+            return null; // already fetched or no more data to fetch
+
+        P result;
+
+        URL url = nextRequest.url();
+        try {
+            GitHubResponse<P> nextResponse = sendNextRequest();
+            assert nextResponse.body() != null;
+            result = nextResponse.body();
+            updateNextRequest(nextResponse);
+        } catch (IOException e) {
+            // Iterators do not throw IOExceptions, so we wrap any IOException
+            // in a runtime GHException to bubble out if needed.
+            throw new GHException("Failed to retrieve " + url, e);
+        }
+        return result;
+    }
+
+    @NotNull protected GitHubResponse<P> sendNextRequest() throws IOException {
+        return client.sendRequest(nextRequest,
+                (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, pageType));
+    }
+
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubPage.java b/src/main/java/org/kohsuke/github/GitHubPage.java
new file mode 100644
index 0000000000..0125b911a7
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubPage.java
@@ -0,0 +1,16 @@
+package org.kohsuke.github;
+
+/**
+ * A page of results from GitHub.
+ *
+ * @param <I>
+ *            the type of items on the page.
+ */
+interface GitHubPage<I> {
+    /**
+     * Wraps up the retrieved object and return them. Only called once.
+     *
+     * @return the items
+     */
+    I[] getItems();
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java b/src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java
deleted file mode 100644
index f8fc7e4907..0000000000
--- a/src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java
+++ /dev/null
@@ -1,93 +0,0 @@
-package org.kohsuke.github;
-
-import java.io.IOException;
-import java.util.function.Consumer;
-
-import javax.annotation.Nonnull;
-
-// TODO: Auto-generated Javadoc
-/**
- * {@link PagedIterable} implementation that take a {@link Consumer} that initializes all the items on each page as they
- * are retrieved.
- *
- * {@link GitHubPageContentsIterable} is immutable and thread-safe, but the iterator returned from {@link #iterator()}
- * is not. Any one instance of iterator should only be called from a single thread.
- *
- * @author Liam Newman
- * @param <T>
- *            the type of items on each page
- */
-class GitHubPageContentsIterable<T> extends PagedIterable<T> {
-
-    /**
-     * This class is not thread-safe. Any one instance should only be called from a single thread.
-     */
-    private class GitHubPageContentsIterator extends PagedIterator<T> {
-
-        public GitHubPageContentsIterator(GitHubPageIterator<T[]> iterator, Consumer<T> itemInitializer) {
-            super(iterator, itemInitializer);
-        }
-
-        /**
-         * Gets the {@link GitHubResponse} for the last page received.
-         *
-         * @return the {@link GitHubResponse} for the last page received.
-         */
-        private GitHubResponse<T[]> lastResponse() {
-            return ((GitHubPageIterator<T[]>) base).finalResponse();
-        }
-    }
-
-    private final GitHubClient client;
-    private final Consumer<T> itemInitializer;
-    private final Class<T[]> receiverType;
-    private final GitHubRequest request;
-
-    /**
-     * Instantiates a new git hub page contents iterable.
-     *
-     * @param client
-     *            the client
-     * @param request
-     *            the request
-     * @param receiverType
-     *            the receiver type
-     * @param itemInitializer
-     *            the item initializer
-     */
-    GitHubPageContentsIterable(GitHubClient client,
-            GitHubRequest request,
-            Class<T[]> receiverType,
-            Consumer<T> itemInitializer) {
-        this.client = client;
-        this.request = request;
-        this.receiverType = receiverType;
-        this.itemInitializer = itemInitializer;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Override
-    @Nonnull
-    public PagedIterator<T> _iterator(int pageSize) {
-        final GitHubPageIterator<T[]> iterator = GitHubPageIterator.create(client, receiverType, request, pageSize);
-        return new GitHubPageContentsIterator(iterator, itemInitializer);
-    }
-
-    /**
-     * Eagerly walk {@link Iterable} and return the result in a {@link GitHubResponse} containing an array of {@code T}
-     * items.
-     *
-     * @return the last response with an array containing all the results from all pages.
-     * @throws IOException
-     *             if an I/O exception occurs.
-     */
-    @Nonnull
-    GitHubResponse<T[]> toResponse() throws IOException {
-        GitHubPageContentsIterator iterator = (GitHubPageContentsIterator) iterator();
-        T[] items = toArray(iterator);
-        GitHubResponse<T[]> lastResponse = iterator.lastResponse();
-        return new GitHubResponse<>(lastResponse, items);
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java b/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
new file mode 100644
index 0000000000..54cdcd220d
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
@@ -0,0 +1,138 @@
+package org.kohsuke.github;
+
+import java.util.*;
+
+import javax.annotation.Nonnull;
+
+/**
+ * This class is not thread-safe. Any one instance should only be called from a single thread.
+ */
+class GitHubPageItemIterator<Page extends GitHubPage<Item>, Item> implements Iterator<Item> {
+
+    /**
+     * Current batch of items. Each time {@link #next()} is called the next item in this array will be returned. After
+     * the last item of the array is returned, when {@link #next()} is called again, a new page of items will be fetched
+     * and iterating will continue from the first item in the new page.
+     *
+     * @see #fetchNext() {@link #fetchNext()} for details on how this field is used.
+     */
+    private Page currentPage;
+
+    /**
+     * The index of the next item on the page, the item that will be returned when {@link #next()} is called.
+     *
+     * @see #fetchNext() {@link #fetchNext()} for details on how this field is used.
+     */
+    private int nextItemIndex;
+
+    private final GitHubPageIterator<Page, Item> pageIterator;
+
+    GitHubPageItemIterator(GitHubPageIterator<Page, Item> pageIterator) {
+        this.pageIterator = pageIterator;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasNext() {
+        return peek() != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Item next() {
+        Item result = peek();
+        if (result == null)
+            throw new NoSuchElementException();
+        nextItemIndex++;
+        return result;
+    }
+
+    /**
+     * Gets the next page worth of data.
+     *
+     * @return the list
+     */
+    public List<Item> nextPage() {
+        return Arrays.asList(nextPageArray());
+    }
+
+    /**
+     *
+     * @return
+     */
+    public Item peek() {
+        Item result = lookupItem();
+        if (result == null && pageIterator.hasNext()) {
+            result = fetchNext();
+        }
+        return result;
+    }
+
+    /**
+     * Fetch is called at the start of {@link #next()} or {@link #hasNext()} to fetch another page of data if it is
+     * needed and available.
+     * <p>
+     * If there is no current page yet (at the start of iterating), a page is fetched. If {@link #nextItemIndex} points
+     * to an item in the current page array, the state is valid - no more work is needed. If {@link #nextItemIndex} is
+     * greater than the last index in the current page array, the method checks if there is another page of data
+     * available.
+     * </p>
+     * <p>
+     * If there is another page, get that page of data and reset the check {@link #nextItemIndex} to the start of the
+     * new page.
+     * </p>
+     * <p>
+     * If no more pages are available, leave the page and index unchanged. In this case, {@link #hasNext()} will return
+     * {@code false} and {@link #next()} will throw an exception.
+     * </p>
+     */
+    private Item fetchNext() {
+        // On first call, always get next page (may be empty array)
+        currentPage = Objects.requireNonNull(pageIterator.next());
+        nextItemIndex = 0;
+        return lookupItem();
+    }
+
+    private Item lookupItem() {
+        return currentPage != null && currentPage.getItems().length > nextItemIndex
+                ? currentPage.getItems()[nextItemIndex]
+                : null;
+    }
+
+    /**
+     * Gets the next page worth of data.
+     *
+     * @return the list
+     */
+    protected Page currentPage() {
+        peek();
+        return currentPage;
+    }
+
+    /**
+     * Gets the next page worth of data.
+     *
+     * @return the list
+     */
+    @Nonnull
+    Item[] nextPageArray() {
+        // if we have not fetched any pages yet, always fetch.
+        // If we have fetched at least one page, check hasNext()
+        if (currentPage == null) {
+            peek();
+        } else if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        // Current should never be null after fetch
+        Objects.requireNonNull(currentPage);
+        Item[] r = currentPage.getItems();
+        if (nextItemIndex != 0) {
+            r = Arrays.copyOfRange(r, nextItemIndex, r.length);
+        }
+        nextItemIndex = currentPage.getItems().length;
+        return r;
+    }
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubPageIterator.java b/src/main/java/org/kohsuke/github/GitHubPageIterator.java
index 4a831bf3f8..cabe9a1d1b 100644
--- a/src/main/java/org/kohsuke/github/GitHubPageIterator.java
+++ b/src/main/java/org/kohsuke/github/GitHubPageIterator.java
@@ -1,63 +1,33 @@
 package org.kohsuke.github;
 
-import java.io.IOException;
-import java.net.URL;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.function.Consumer;
 
 import javax.annotation.Nonnull;
 
-// TODO: Auto-generated Javadoc
 /**
- * May be used for any item that has pagination information. Iterates over paginated {@code T} objects (not the items
- * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse}{@code <T>}
- * after iterating completes.
+ * May be used for any item that has pagination information. Iterates over paginated {@code P} objects (not the items
+ * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse} {@code
+ *
+<P>
+ * } after iterating completes.
  *
  * Works for array responses, also works for search results which are single instances with an array of items inside.
  *
  * This class is not thread-safe. Any one instance should only be called from a single thread.
  *
  * @author Liam Newman
- * @param <T>
+ * @param <P>
  *            type of each page (not the items in the page).
  */
-class GitHubPageIterator<T> implements Iterator<T> {
-
-    /**
-     * Loads paginated resources.
-     *
-     * @param <T>
-     *            type of each page (not the items in the page).
-     * @param client
-     *            the {@link GitHubClient} from which to request responses
-     * @param type
-     *            type of each page (not the items in the page).
-     * @param request
-     *            the request
-     * @param pageSize
-     *            the page size
-     * @return iterator
-     */
-    static <T> GitHubPageIterator<T> create(GitHubClient client, Class<T> type, GitHubRequest request, int pageSize) {
-
-        if (pageSize > 0) {
-            GitHubRequest.Builder<?> builder = request.toBuilder().with("per_page", pageSize);
-            request = builder.build();
-        }
+class GitHubPageIterator<P extends GitHubPage<Item>, Item> implements Iterator<P> {
 
-        if (!"GET".equals(request.method())) {
-            throw new IllegalArgumentException("Request method \"GET\" is required for page iterator.");
-        }
-
-        return new GitHubPageIterator<>(client, type, request);
+    static <P extends GitHubPage<Item>, Item> GitHubPageIterator<P, Item> ofSingleton(final P page) {
+        return new GitHubPageIterator<>(page);
     }
-    private final GitHubClient client;
 
-    /**
-     * When done iterating over pages, it is on rare occasions useful to be able to get information from the final
-     * response that was retrieved.
-     */
-    private GitHubResponse<T> finalResponse = null;
+    private final Consumer<Item> itemInitializer;
 
     /**
      * The page that will be returned when {@link #next()} is called.
@@ -66,23 +36,20 @@ static <T> GitHubPageIterator<T> create(GitHubClient client, Class<T> type, GitH
      * Will be {@code null} after {@link #next()} is called.
      * </p>
      * <p>
-     * Will not be {@code null} after {@link #fetch()} is called if a new page was fetched.
+     * Will not be {@code null} after {@link #fetchNext()} is called if a new page was fetched.
      * </p>
      */
-    private T next;
+    protected P next;
+    protected final Class<P> pageType;
 
-    /**
-     * The request that will be sent when to get a new response page if {@link #next} is {@code null}. Will be
-     * {@code null} when there are no more pages to fetch.
-     */
-    private GitHubRequest nextRequest;
-
-    private final Class<T> type;
+    private GitHubPageIterator(P page) {
+        this((Class<P>) page.getClass(), null);
+        this.next = page;
+    }
 
-    private GitHubPageIterator(GitHubClient client, Class<T> type, GitHubRequest request) {
-        this.client = client;
-        this.type = type;
-        this.nextRequest = request;
+    protected GitHubPageIterator(Class<P> pageType, Consumer<Item> itemInitializer) {
+        this.pageType = pageType;
+        this.itemInitializer = itemInitializer;
     }
 
     /**
@@ -90,91 +57,62 @@ private GitHubPageIterator(GitHubClient client, Class<T> type, GitHubRequest req
      *
      * @return the final response of the iterator.
      */
-    public GitHubResponse<T> finalResponse() {
-        if (hasNext()) {
-            throw new GHException("Final response is not available until after iterator is done.");
-        }
-        return finalResponse;
+    public GitHubResponse<P> finalResponse() {
+        return null;
     }
 
     /**
      * {@inheritDoc}
      */
     public boolean hasNext() {
-        fetch();
-        return next != null;
+        return peek() != null;
     }
 
     /**
-     * Gets the next page.
-     *
-     * @return the next page.
+     * {@inheritDoc}
      */
     @Nonnull
-    public T next() {
-        fetch();
-        T result = next;
+    public P next() {
+        P result = peek();
         if (result == null)
             throw new NoSuchElementException();
-        // If this is the last page, keep the response
         next = null;
         return result;
     }
 
     /**
-     * Fetch is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it is
-     * needed.
-     * <p>
-     * If {@link #next} is not {@code null}, no further action is needed. If {@link #next} is {@code null} and
-     * {@link #nextRequest} is {@code null}, there are no more pages to fetch.
-     * </p>
-     * <p>
-     * Otherwise, a new response page is fetched using {@link #nextRequest}. The response is then checked to see if
-     * there is a page after it and {@link #nextRequest} is updated to point to it. If there are no pages available
-     * after the current response, {@link #nextRequest} is set to {@code null}.
-     * </p>
+     *
+     * @return
      */
-    private void fetch() {
-        if (next != null)
-            return; // already fetched
-        if (nextRequest == null)
-            return; // no more data to fetch
-
-        URL url = nextRequest.url();
-        try {
-            GitHubResponse<T> nextResponse = client.sendRequest(nextRequest,
-                    (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, type));
-            assert nextResponse.body() != null;
-            next = nextResponse.body();
-            nextRequest = findNextURL(nextRequest, nextResponse);
-            if (nextRequest == null) {
-                finalResponse = nextResponse;
+    public P peek() {
+        if (next == null) {
+            P result = fetchNext();
+            if (result != null) {
+                next = result;
+                initializeItems();
             }
-        } catch (IOException e) {
-            // Iterators do not throw IOExceptions, so we wrap any IOException
-            // in a runtime GHException to bubble out if needed.
-            throw new GHException("Failed to retrieve " + url, e);
         }
+        return next;
     }
 
     /**
-     * Locate the next page from the pagination "Link" tag.
+     * This method initializes items with local data after they are fetched. It is up to the implementer to decide what
+     * local data to apply.
+     *
      */
-    private GitHubRequest findNextURL(GitHubRequest nextRequest, GitHubResponse<T> nextResponse) {
-        GitHubRequest result = null;
-        String link = nextResponse.header("Link");
-        if (link != null) {
-            for (String token : link.split(", ")) {
-                if (token.endsWith("rel=\"next\"")) {
-                    // found the next page. This should look something like
-                    // <https://api.github.com/repos?page=3&per_page=100>; rel="next"
-                    int idx = token.indexOf('>');
-                    result = nextRequest.toBuilder().setRawUrlPath(token.substring(1, idx)).build();
-                    break;
-                }
+    private void initializeItems() {
+        if (itemInitializer != null) {
+            for (Item item : next.getItems()) {
+                itemInitializer.accept(item);
             }
         }
-        return result;
     }
 
+    /**
+     * This method is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it
+     * is needed.
+     */
+    protected P fetchNext() {
+        return null;
+    }
 }
diff --git a/src/main/java/org/kohsuke/github/GitHubResponse.java b/src/main/java/org/kohsuke/github/GitHubResponse.java
index 8ac65391f7..180f04998d 100644
--- a/src/main/java/org/kohsuke/github/GitHubResponse.java
+++ b/src/main/java/org/kohsuke/github/GitHubResponse.java
@@ -162,7 +162,7 @@ static <T> T parseBody(GitHubConnectorResponse connectorResponse, T instance) th
      * @param body
      *            the body
      */
-    GitHubResponse(GitHubResponse<T> response, @CheckForNull T body) {
+    GitHubResponse(GitHubResponse<?> response, @CheckForNull T body) {
         this.statusCode = response.statusCode();
         this.headers = response.headers;
         this.body = body;
diff --git a/src/main/java/org/kohsuke/github/PagedIterable.java b/src/main/java/org/kohsuke/github/PagedIterable.java
index a916af8009..e598832361 100644
--- a/src/main/java/org/kohsuke/github/PagedIterable.java
+++ b/src/main/java/org/kohsuke/github/PagedIterable.java
@@ -1,11 +1,6 @@
 package org.kohsuke.github;
 
 import java.io.IOException;
-import java.lang.reflect.Array;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 
@@ -20,146 +15,47 @@
  * @param <T>
  *            the type of items on each page
  */
-public abstract class PagedIterable<T> implements Iterable<T> {
-    /**
-     * Page size. 0 is default.
-     */
-    private int pageSize = 0;
+public class PagedIterable<T> implements Iterable<T> {
+
+    private final GitHubEndpointIterable<?, T> paginatedEndpoint;
 
     /**
-     * Instantiate a PagedIterable.
+     * Instantiates a new git hub page contents iterable.
      */
-    public PagedIterable() {
+    PagedIterable(GitHubEndpointIterable<?, T> paginatedEndpoint) {
+        this.paginatedEndpoint = paginatedEndpoint;
     }
 
-    /**
-     * Iterator over page items.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    public abstract PagedIterator<T> _iterator(int pageSize);
+    public PagedIterator<T> _iterator(int pageSize) {
+        throw new RuntimeException("No longer used.");
+    }
 
-    /**
-     * Returns an iterator over elements of type {@code T}.
-     *
-     * @return an Iterator.
-     */
     @Nonnull
     public final PagedIterator<T> iterator() {
-        return _iterator(pageSize);
+        return new PagedIterator<>(paginatedEndpoint.itemIterator());
     }
 
-    /**
-     * Eagerly walk {@link Iterable} and return the result in an array.
-     *
-     * @return the list
-     * @throws IOException
-     *             if an I/O exception occurs.
-     */
     @Nonnull
     public T[] toArray() throws IOException {
-        return toArray(iterator());
+        return paginatedEndpoint.toArray();
     }
 
-    /**
-     * Eagerly walk {@link Iterable} and return the result in a list.
-     *
-     * @return the list
-     * @throws IOException
-     *             if an I/O Exception occurs
-     */
     @Nonnull
     public List<T> toList() throws IOException {
-        return Collections.unmodifiableList(Arrays.asList(this.toArray()));
+        return paginatedEndpoint.toList();
     }
 
-    /**
-     * Eagerly walk {@link Iterable} and return the result in a set.
-     *
-     * @return the set
-     * @throws IOException
-     *             if an I/O Exception occurs
-     */
     @Nonnull
     public Set<T> toSet() throws IOException {
-        return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(this.toArray())));
+        return paginatedEndpoint.toSet();
     }
 
-    /**
-     * Sets the pagination size.
-     *
-     * <p>
-     * When set to non-zero, each API call will retrieve this many entries.
-     *
-     * @param size
-     *            the size
-     * @return the paged iterable
-     */
-    public PagedIterable<T> withPageSize(int size) {
-        this.pageSize = size;
+    public PagedIterable<T> withPageSize(int i) {
+        paginatedEndpoint.withPageSize(i);
         return this;
     }
 
-    /**
-     * Concatenates a list of arrays into a single array.
-     *
-     * @param type
-     *            the type of array to be returned.
-     * @param pages
-     *            the list of arrays to be concatenated.
-     * @param totalLength
-     *            the total length of the returned array.
-     * @return an array containing all elements from all pages.
-     */
-    @Nonnull
-    private T[] concatenatePages(Class<T[]> type, List<T[]> pages, int totalLength) {
-
-        T[] result = type.cast(Array.newInstance(type.getComponentType(), totalLength));
-
-        int position = 0;
-        for (T[] page : pages) {
-            final int pageLength = Array.getLength(page);
-            System.arraycopy(page, 0, result, position, pageLength);
-            position += pageLength;
-        }
-        return result;
-    }
-
-    /**
-     * Eagerly walk {@link PagedIterator} and return the result in an array.
-     *
-     * @param iterator
-     *            the {@link PagedIterator} to read
-     * @return an array of all elements from the {@link PagedIterator}
-     * @throws IOException
-     *             if an I/O exception occurs.
-     */
-    protected T[] toArray(final PagedIterator<T> iterator) throws IOException {
-        try {
-            ArrayList<T[]> pages = new ArrayList<>();
-            int totalSize = 0;
-            T[] item;
-            do {
-                item = iterator.nextPageArray();
-                totalSize += Array.getLength(item);
-                pages.add(item);
-            } while (iterator.hasNext());
-
-            Class<T[]> type = (Class<T[]>) item.getClass();
-
-            return concatenatePages(type, pages, totalSize);
-        } catch (GHException e) {
-            // if there was an exception inside the iterator it is wrapped as a GHException
-            // if the wrapped exception is an IOException, throw that
-            if (e.getCause() instanceof IOException) {
-                throw (IOException) e.getCause();
-            } else {
-                throw e;
-            }
-        }
+    GitHubResponse<T[]> toResponse() throws IOException {
+        return paginatedEndpoint.toResponse();
     }
-
 }
diff --git a/src/main/java/org/kohsuke/github/PagedIterator.java b/src/main/java/org/kohsuke/github/PagedIterator.java
index ac6e54e826..d4c334692d 100644
--- a/src/main/java/org/kohsuke/github/PagedIterator.java
+++ b/src/main/java/org/kohsuke/github/PagedIterator.java
@@ -1,14 +1,7 @@
 package org.kohsuke.github;
 
-import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Objects;
-import java.util.function.Consumer;
-
-import javax.annotation.CheckForNull;
-import javax.annotation.Nonnull;
 
 // TODO: Auto-generated Javadoc
 /**
@@ -26,133 +19,34 @@
  */
 public class PagedIterator<T> implements Iterator<T> {
 
-    /**
-     * Current batch of items. Each time {@link #next()} is called the next item in this array will be returned. After
-     * the last item of the array is returned, when {@link #next()} is called again, a new page of items will be fetched
-     * and iterating will continue from the first item in the new page.
-     *
-     * @see #fetch() {@link #fetch()} for details on how this field is used.
-     */
-    private T[] currentPage;
-
-    @CheckForNull
-    private final Consumer<T> itemInitializer;
-
-    /**
-     * The index of the next item on the page, the item that will be returned when {@link #next()} is called.
-     *
-     * @see #fetch() {@link #fetch()} for details on how this field is used.
-     */
-    private int nextItemIndex;
-
-    /** The base. */
-    @Nonnull
-    protected final Iterator<T[]> base;
+    private final GitHubPageItemIterator<?, T> endpointIterator;
 
     /**
      * Instantiates a new paged iterator.
      *
-     * @param base
+     * @param endpointIterator
      *            the base
-     * @param itemInitializer
-     *            the item initializer
      */
-    PagedIterator(@Nonnull Iterator<T[]> base, @CheckForNull Consumer<T> itemInitializer) {
-        this.base = base;
-        this.itemInitializer = itemInitializer;
+    PagedIterator(GitHubPageItemIterator<?, T> endpointIterator) {
+        this.endpointIterator = endpointIterator;
     }
 
-    /**
-     * {@inheritDoc}
-     */
     public boolean hasNext() {
-        fetch();
-        return (currentPage != null && currentPage.length > nextItemIndex);
+        return endpointIterator.hasNext();
     }
 
-    /**
-     * {@inheritDoc}
-     */
     public T next() {
-        if (!hasNext())
-            throw new NoSuchElementException();
-        return currentPage[nextItemIndex++];
+        return endpointIterator.next();
     }
 
     /**
-     * Gets the next page worth of data.
+     * Get the next page of items.
      *
-     * @return the list
+     * @return a list of the next page of items.
+     * @deprecated use PagedIterable.pageIterator().
      */
+    @Deprecated
     public List<T> nextPage() {
-        return Arrays.asList(nextPageArray());
-    }
-
-    /**
-     * Fetch is called at the start of {@link #next()} or {@link #hasNext()} to fetch another page of data if it is
-     * needed and available.
-     * <p>
-     * If there is no current page yet (at the start of iterating), a page is fetched. If {@link #nextItemIndex} points
-     * to an item in the current page array, the state is valid - no more work is needed. If {@link #nextItemIndex} is
-     * greater than the last index in the current page array, the method checks if there is another page of data
-     * available.
-     * </p>
-     * <p>
-     * If there is another page, get that page of data and reset the check {@link #nextItemIndex} to the start of the
-     * new page.
-     * </p>
-     * <p>
-     * If no more pages are available, leave the page and index unchanged. In this case, {@link #hasNext()} will return
-     * {@code false} and {@link #next()} will throw an exception.
-     * </p>
-     */
-    private void fetch() {
-        if ((currentPage == null || currentPage.length <= nextItemIndex) && base.hasNext()) {
-            // On first call, always get next page (may be empty array)
-            T[] result = Objects.requireNonNull(base.next());
-            wrapUp(result);
-            currentPage = result;
-            nextItemIndex = 0;
-        }
-    }
-
-    /**
-     * This poorly named method, initializes items with local data after they are fetched. It is up to the implementer
-     * to decide what local data to apply.
-     *
-     * @param page
-     *            the page of items to be initialized
-     */
-    protected void wrapUp(T[] page) {
-        if (itemInitializer != null) {
-            for (T item : page) {
-                itemInitializer.accept(item);
-            }
-        }
-    }
-
-    /**
-     * Gets the next page worth of data.
-     *
-     * @return the list
-     */
-    @Nonnull
-    T[] nextPageArray() {
-        // if we have not fetched any pages yet, always fetch.
-        // If we have fetched at least one page, check hasNext()
-        if (currentPage == null) {
-            fetch();
-        } else if (!hasNext()) {
-            throw new NoSuchElementException();
-        }
-
-        // Current should never be null after fetch
-        Objects.requireNonNull(currentPage);
-        T[] r = currentPage;
-        if (nextItemIndex != 0) {
-            r = Arrays.copyOfRange(r, nextItemIndex, r.length);
-        }
-        nextItemIndex = currentPage.length;
-        return r;
+        return endpointIterator.nextPage();
     }
 }
diff --git a/src/main/java/org/kohsuke/github/PagedSearchIterable.java b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
index 8c6d00a26d..29c62e818c 100644
--- a/src/main/java/org/kohsuke/github/PagedSearchIterable.java
+++ b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
@@ -2,10 +2,6 @@
 
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
-import java.util.Iterator;
-
-import javax.annotation.Nonnull;
-
 // TODO: Auto-generated Javadoc
 /**
  * {@link PagedIterable} enhanced to report search result specific information.
@@ -19,46 +15,15 @@
                 "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" },
         justification = "Constructed by JSON API")
 public class PagedSearchIterable<T> extends PagedIterable<T> {
-    private final Class<? extends SearchResult<T>> receiverType;
 
-    private final GitHubRequest request;
+    private final GitHubEndpointIterable<? extends SearchResult<T>, T> paginatedEndpoint;
 
     /**
-     * As soon as we have any result fetched, it's set here so that we can report the total count.
-     */
-    private SearchResult<T> result;
-
-    private final transient GitHub root;
-
-    /**
-     * Instantiates a new paged search iterable.
-     *
-     * @param root
-     *            the root
-     * @param request
-     *            the request
-     * @param receiverType
-     *            the receiver type
+     * Instantiates a new git hub page contents iterable.
      */
-    PagedSearchIterable(GitHub root, GitHubRequest request, Class<? extends SearchResult<T>> receiverType) {
-        this.root = root;
-        this.request = request;
-        this.receiverType = receiverType;
-    }
-
-    /**
-     * Iterator.
-     *
-     * @param pageSize
-     *            the page size
-     * @return the paged iterator
-     */
-    @Nonnull
-    @Override
-    public PagedIterator<T> _iterator(int pageSize) {
-        final Iterator<T[]> adapter = adapt(
-                GitHubPageIterator.create(root.getClient(), receiverType, request, pageSize));
-        return new PagedIterator<T>(adapter, null);
+    <Result extends SearchResult<T>> PagedSearchIterable(GitHubEndpointIterable<Result, T> paginatedEndpoint) {
+        super(paginatedEndpoint);
+        this.paginatedEndpoint = paginatedEndpoint;
     }
 
     /**
@@ -67,8 +32,8 @@ public PagedIterator<T> _iterator(int pageSize) {
      * @return the total count
      */
     public int getTotalCount() {
-        populate();
-        return result.totalCount;
+        // populate();
+        return paginatedEndpoint.itemIterator().currentPage().totalCount;
     }
 
     /**
@@ -77,46 +42,7 @@ public int getTotalCount() {
      * @return the boolean
      */
     public boolean isIncomplete() {
-        populate();
-        return result.incompleteResults;
-    }
-
-    /**
-     * With page size.
-     *
-     * @param size
-     *            the size
-     * @return the paged search iterable
-     */
-    @Override
-    public PagedSearchIterable<T> withPageSize(int size) {
-        return (PagedSearchIterable<T>) super.withPageSize(size);
-    }
-
-    private void populate() {
-        if (result == null)
-            iterator().hasNext();
-    }
-
-    /**
-     * Adapts {@link Iterator}.
-     *
-     * @param base
-     *            the base
-     * @return the iterator
-     */
-    protected Iterator<T[]> adapt(final Iterator<? extends SearchResult<T>> base) {
-        return new Iterator<T[]>() {
-            public boolean hasNext() {
-                return base.hasNext();
-            }
-
-            public T[] next() {
-                SearchResult<T> v = base.next();
-                if (result == null)
-                    result = v;
-                return v.getItems(root);
-            }
-        };
+        // populate();
+        return paginatedEndpoint.itemIterator().currentPage().incompleteResults;
     }
 }
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index 95f0366ebd..e6fd73168d 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -189,16 +189,42 @@ public void sendGraphQL() throws IOException {
      * or {@link Iterator#hasNext()} are called.
      * </p>
      *
+     * @param <P>
+     *            the page type for the pages returned from
      * @param <R>
      *            the element type for the pages returned from
-     * @param type
+     * @param pageType
      *            the type of the pages to retrieve.
      * @param itemInitializer
      *            the consumer to execute on each paged item retrieved.
      * @return the {@link PagedIterable} for this builder.
      */
-    public <R> PagedIterable<R> toIterable(Class<R[]> type, Consumer<R> itemInitializer) {
-        return new GitHubPageContentsIterable<>(client, build(), type, itemInitializer);
+    public <P extends GitHubPage<R>, R> PagedIterable<R> toIterable(Class<P> pageType,
+            Class<R> itemType,
+            Consumer<R> itemInitializer) {
+        GitHubRequest request = build();
+        return new PagedIterable<>(new GitHubEndpointIterable<>(client, request, pageType, itemType, itemInitializer));
+    }
 
+    /**
+     * Creates {@link PagedIterable <R>} from this builder using the provided {@link Consumer}{@code <R>}.
+     * <p>
+     * This method and the {@link PagedIterable <R>} do not actually begin fetching data until {@link Iterator#next()}
+     * or {@link Iterator#hasNext()} are called.
+     * </p>
+     *
+     * @param <R>
+     *            the element type for the pages returned from
+     * @param receiverType
+     *            the type of the array to retrieve.
+     * @param itemInitializer
+     *            the consumer to execute on each paged item retrieved.
+     * @return the {@link PagedIterable} for this builder.
+     */
+    public <R> PagedIterable<R> toIterable(Class<R[]> receiverType, Consumer<R> itemInitializer) {
+        GitHubRequest request = build();
+        return new PagedIterable<>(
+                GitHubEndpointIterable.ofArrayEndpoint(client, request, receiverType, itemInitializer));
     }
+
 }
diff --git a/src/main/java/org/kohsuke/github/SearchResult.java b/src/main/java/org/kohsuke/github/SearchResult.java
index fe7e350439..2707b86753 100644
--- a/src/main/java/org/kohsuke/github/SearchResult.java
+++ b/src/main/java/org/kohsuke/github/SearchResult.java
@@ -8,7 +8,7 @@
  * @param <T>
  *            the generic type
  */
-abstract class SearchResult<T> {
+abstract class SearchResult<T> implements GitHubPage<T> {
 
     /** The incomplete results. */
     boolean incompleteResults;
@@ -16,6 +16,10 @@ abstract class SearchResult<T> {
     /** The total count. */
     int totalCount;
 
+    public T[] getItems() {
+        return getItems(null);
+    }
+
     /**
      * Wraps up the retrieved object and return them. Only called once.
      *
diff --git a/src/test/resources/no-reflect-and-serialization-list b/src/test/resources/no-reflect-and-serialization-list
index 4ad893272c..86aae594b3 100644
--- a/src/test/resources/no-reflect-and-serialization-list
+++ b/src/test/resources/no-reflect-and-serialization-list
@@ -25,7 +25,7 @@ org.kohsuke.github.GitHubClient$BodyHandler
 org.kohsuke.github.GitHubClient$GHApiInfo
 org.kohsuke.github.GitHubClient$RetryRequestException
 org.kohsuke.github.GitHubConnectorResponseErrorHandler
-org.kohsuke.github.GitHubPageIterator
+org.kohsuke.github.GitHubEndpointPageIterator
 org.kohsuke.github.GitHubRateLimitChecker
 org.kohsuke.github.GitHubRateLimitHandler
 org.kohsuke.github.GitHubRateLimitHandler$1

From 78d1a9ed0079f184c3256d9b8751237a0212a05d Mon Sep 17 00:00:00 2001
From: Liam Newman <bitwiseman@gmail.com>
Date: Sat, 3 May 2025 23:29:30 -0700
Subject: [PATCH 2/4] Flatten tree

---
 .../github/GitHubEndpointIterable.java        |  10 +-
 .../github/GitHubEndpointPageIterator.java    |  83 +++++++++++-
 .../github/GitHubPageItemIterator.java        |  57 +++------
 .../kohsuke/github/GitHubPageIterator.java    | 118 ------------------
 .../kohsuke/github/PagedSearchIterable.java   |   4 +-
 5 files changed, 103 insertions(+), 169 deletions(-)
 delete mode 100644 src/main/java/org/kohsuke/github/GitHubPageIterator.java

diff --git a/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java b/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
index 804a497a1d..6f0676d0c5 100644
--- a/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
+++ b/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
@@ -107,8 +107,8 @@ static <P extends GitHubPage<I>, I> GitHubEndpointIterable<P, I> ofSingleton(P p
         return new GitHubEndpointIterable<>(null, null, (Class<P>) page.getClass(), itemType, null) {
             @Nonnull
             @Override
-            public GitHubPageIterator<P, I> pageIterator() {
-                return GitHubPageIterator.ofSingleton(page);
+            public GitHubEndpointPageIterator<P, I> pageIterator() {
+                return GitHubEndpointPageIterator.ofSingleton(page);
             }
         };
     }
@@ -165,7 +165,7 @@ public final Iterator<Item> iterator() {
      * @return
      */
     @Nonnull
-    public GitHubPageIterator<Page, Item> pageIterator() {
+    public GitHubEndpointPageIterator<Page, Item> pageIterator() {
         return new GitHubEndpointPageIterator<>(client, pageType, request, pageSize, itemInitializer);
     }
 
@@ -251,7 +251,7 @@ private Item[] concatenatePages(List<Item[]> pages, int totalLength) {
      * @throws IOException
      *             if an I/O exception occurs.
      */
-    private Item[] toArray(final GitHubPageIterator<Page, Item> iterator, Class<Item> itemType) throws IOException {
+    private Item[] toArray(final GitHubEndpointPageIterator<Page, Item> iterator, Class<Item> itemType) throws IOException {
         try {
             ArrayList<Item[]> pages = new ArrayList<>();
             int totalSize = 0;
@@ -284,7 +284,7 @@ private Item[] toArray(final GitHubPageIterator<Page, Item> iterator, Class<Item
      */
     @Nonnull
     final GitHubResponse<Item[]> toResponse() throws IOException {
-        GitHubPageIterator<Page, Item> iterator = pageIterator();
+        GitHubEndpointPageIterator<Page, Item> iterator = pageIterator();
         Item[] items = toArray(iterator, itemType);
         GitHubResponse<Page> lastResponse = iterator.finalResponse();
         return new GitHubResponse<>(lastResponse, items);
diff --git a/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java b/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
index 9440024b77..5f50835e6e 100644
--- a/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
+++ b/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
@@ -2,8 +2,10 @@
 
 import org.jetbrains.annotations.NotNull;
 
+import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.net.URL;
+import java.util.NoSuchElementException;
 import java.util.function.Consumer;
 
 /**
@@ -21,8 +23,21 @@
  * @param <P>
  *            type of each page (not the items in the page).
  */
-class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> extends GitHubPageIterator<P, Item> {
+class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> implements java.util.Iterator<P> {
 
+    protected final Class<P> pageType;
+    private final Consumer<Item> itemInitializer;
+    /**
+     * The page that will be returned when {@link #next()} is called.
+     *
+     * <p>
+     * Will be {@code null} after {@link #next()} is called.
+     * </p>
+     * <p>
+     * Will not be {@code null} after {@link #fetchNext()} is called if a new page was fetched.
+     * </p>
+     */
+    private P next;
     /**
      * When done iterating over pages, it is on rare occasions useful to be able to get information from the final
      * response that was retrieved.
@@ -37,12 +52,18 @@ class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> extends GitHu
      */
     protected GitHubRequest nextRequest;
 
+    private GitHubEndpointPageIterator(P page) {
+        this(null, (Class<P>)page.getClass(), null, 0, null);
+        this.next = page;
+    }
+
     GitHubEndpointPageIterator(GitHubClient client,
             Class<P> pageType,
             GitHubRequest request,
             int pageSize,
             Consumer<Item> itemInitializer) {
-        super(pageType, itemInitializer);
+        this.pageType = pageType;
+        this.itemInitializer = itemInitializer;
 
         if (pageSize > 0) {
             GitHubRequest.Builder<?> builder = request.toBuilder().with("per_page", pageSize);
@@ -57,6 +78,10 @@ class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> extends GitHu
         this.nextRequest = request;
     }
 
+    static <P extends GitHubPage<Item>, Item> GitHubEndpointPageIterator<P, Item> ofSingleton(final P page) {
+        return new GitHubEndpointPageIterator<>(page);
+    }
+
     /**
      * On rare occasions the final response from iterating is needed.
      *
@@ -97,7 +122,8 @@ private void updateNextRequest(GitHubResponse<P> nextResponse) {
      * Fetch is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it is
      * needed.
      * <p>
-     * If {@link #next} is not {@code null}, no further action is needed. If {@link #next} is {@code null} and
+     * If {@link #next} is not {@code null}, no further action is needed.
+     * If {@link #next} is {@code null} and
      * {@link #nextRequest} is {@code null}, there are no more pages to fetch.
      * </p>
      * <p>
@@ -106,10 +132,9 @@ private void updateNextRequest(GitHubResponse<P> nextResponse) {
      * after the current response, {@link #nextRequest} is set to {@code null}.
      * </p>
      */
-    @Override
     protected P fetchNext() {
-        if (next != null || nextRequest == null)
-            return null; // already fetched or no more data to fetch
+        if (nextRequest == null)
+            return null; // no more data to fetch
 
         P result;
 
@@ -132,4 +157,50 @@ protected P fetchNext() {
                 (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, pageType));
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasNext() {
+        return peek() != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Nonnull
+    public P next() {
+        P result = peek();
+        if (result == null)
+            throw new NoSuchElementException();
+        next = null;
+        return result;
+    }
+
+    /**
+     *
+     * @return
+     */
+    public P peek() {
+        if (next == null) {
+            P result = fetchNext();
+            if (result != null) {
+                next = result;
+                initializeItems();
+            }
+        }
+        return next;
+    }
+
+    /**
+     * This method initializes items with local data after they are fetched. It is up to the implementer to decide what
+     * local data to apply.
+     *
+     */
+    private void initializeItems() {
+        if (itemInitializer != null) {
+            for (Item item : next.getItems()) {
+                itemInitializer.accept(item);
+            }
+        }
+    }
 }
diff --git a/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java b/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
index 54cdcd220d..31869c3dde 100644
--- a/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
+++ b/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
@@ -25,9 +25,9 @@ class GitHubPageItemIterator<Page extends GitHubPage<Item>, Item> implements Ite
      */
     private int nextItemIndex;
 
-    private final GitHubPageIterator<Page, Item> pageIterator;
+    private final GitHubEndpointPageIterator<Page, Item> pageIterator;
 
-    GitHubPageItemIterator(GitHubPageIterator<Page, Item> pageIterator) {
+    GitHubPageItemIterator(GitHubEndpointPageIterator<Page, Item> pageIterator) {
         this.pageIterator = pageIterator;
     }
 
@@ -54,8 +54,24 @@ public Item next() {
      *
      * @return the list
      */
+    @Deprecated
     public List<Item> nextPage() {
-        return Arrays.asList(nextPageArray());
+        // if we have not fetched any pages yet, always fetch.
+        // If we have fetched at least one page, check hasNext()
+        if (currentPage == null) {
+            peek();
+        } else if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        // Current should never be null after fetch
+        Objects.requireNonNull(currentPage);
+        Item[] r = currentPage.getItems();
+        if (nextItemIndex != 0) {
+            r = Arrays.copyOfRange(r, nextItemIndex, r.length);
+        }
+        nextItemIndex = currentPage.getItems().length;
+        return Arrays.asList(r);
     }
 
     /**
@@ -100,39 +116,4 @@ private Item lookupItem() {
                 ? currentPage.getItems()[nextItemIndex]
                 : null;
     }
-
-    /**
-     * Gets the next page worth of data.
-     *
-     * @return the list
-     */
-    protected Page currentPage() {
-        peek();
-        return currentPage;
-    }
-
-    /**
-     * Gets the next page worth of data.
-     *
-     * @return the list
-     */
-    @Nonnull
-    Item[] nextPageArray() {
-        // if we have not fetched any pages yet, always fetch.
-        // If we have fetched at least one page, check hasNext()
-        if (currentPage == null) {
-            peek();
-        } else if (!hasNext()) {
-            throw new NoSuchElementException();
-        }
-
-        // Current should never be null after fetch
-        Objects.requireNonNull(currentPage);
-        Item[] r = currentPage.getItems();
-        if (nextItemIndex != 0) {
-            r = Arrays.copyOfRange(r, nextItemIndex, r.length);
-        }
-        nextItemIndex = currentPage.getItems().length;
-        return r;
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/GitHubPageIterator.java b/src/main/java/org/kohsuke/github/GitHubPageIterator.java
deleted file mode 100644
index cabe9a1d1b..0000000000
--- a/src/main/java/org/kohsuke/github/GitHubPageIterator.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package org.kohsuke.github;
-
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-import java.util.function.Consumer;
-
-import javax.annotation.Nonnull;
-
-/**
- * May be used for any item that has pagination information. Iterates over paginated {@code P} objects (not the items
- * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse} {@code
- *
-<P>
- * } after iterating completes.
- *
- * Works for array responses, also works for search results which are single instances with an array of items inside.
- *
- * This class is not thread-safe. Any one instance should only be called from a single thread.
- *
- * @author Liam Newman
- * @param <P>
- *            type of each page (not the items in the page).
- */
-class GitHubPageIterator<P extends GitHubPage<Item>, Item> implements Iterator<P> {
-
-    static <P extends GitHubPage<Item>, Item> GitHubPageIterator<P, Item> ofSingleton(final P page) {
-        return new GitHubPageIterator<>(page);
-    }
-
-    private final Consumer<Item> itemInitializer;
-
-    /**
-     * The page that will be returned when {@link #next()} is called.
-     *
-     * <p>
-     * Will be {@code null} after {@link #next()} is called.
-     * </p>
-     * <p>
-     * Will not be {@code null} after {@link #fetchNext()} is called if a new page was fetched.
-     * </p>
-     */
-    protected P next;
-    protected final Class<P> pageType;
-
-    private GitHubPageIterator(P page) {
-        this((Class<P>) page.getClass(), null);
-        this.next = page;
-    }
-
-    protected GitHubPageIterator(Class<P> pageType, Consumer<Item> itemInitializer) {
-        this.pageType = pageType;
-        this.itemInitializer = itemInitializer;
-    }
-
-    /**
-     * On rare occasions the final response from iterating is needed.
-     *
-     * @return the final response of the iterator.
-     */
-    public GitHubResponse<P> finalResponse() {
-        return null;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    public boolean hasNext() {
-        return peek() != null;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Nonnull
-    public P next() {
-        P result = peek();
-        if (result == null)
-            throw new NoSuchElementException();
-        next = null;
-        return result;
-    }
-
-    /**
-     *
-     * @return
-     */
-    public P peek() {
-        if (next == null) {
-            P result = fetchNext();
-            if (result != null) {
-                next = result;
-                initializeItems();
-            }
-        }
-        return next;
-    }
-
-    /**
-     * This method initializes items with local data after they are fetched. It is up to the implementer to decide what
-     * local data to apply.
-     *
-     */
-    private void initializeItems() {
-        if (itemInitializer != null) {
-            for (Item item : next.getItems()) {
-                itemInitializer.accept(item);
-            }
-        }
-    }
-
-    /**
-     * This method is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it
-     * is needed.
-     */
-    protected P fetchNext() {
-        return null;
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/PagedSearchIterable.java b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
index 29c62e818c..3f7d5af54d 100644
--- a/src/main/java/org/kohsuke/github/PagedSearchIterable.java
+++ b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
@@ -33,7 +33,7 @@ <Result extends SearchResult<T>> PagedSearchIterable(GitHubEndpointIterable<Resu
      */
     public int getTotalCount() {
         // populate();
-        return paginatedEndpoint.itemIterator().currentPage().totalCount;
+        return paginatedEndpoint.pageIterator().peek().totalCount;
     }
 
     /**
@@ -43,6 +43,6 @@ public int getTotalCount() {
      */
     public boolean isIncomplete() {
         // populate();
-        return paginatedEndpoint.itemIterator().currentPage().incompleteResults;
+        return paginatedEndpoint.pageIterator().peek().incompleteResults;
     }
 }

From 024cb8f935d1b7a190e72f6ab59b803fbf5db672 Mon Sep 17 00:00:00 2001
From: Liam Newman <bitwiseman@gmail.com>
Date: Sun, 4 May 2025 00:25:53 -0700
Subject: [PATCH 3/4] Rename

---
 .../org/kohsuke/github/GHAppInstallation.java |   2 +-
 .../github/GHAppInstallationsIterable.java    |   2 +-
 .../kohsuke/github/GHArtifactsIterable.java   |   2 +-
 .../GHAuthenticatedAppInstallation.java       |   2 +-
 .../kohsuke/github/GHCheckRunsIterable.java   |   2 +-
 .../kohsuke/github/GHCommitFileIterable.java  |   8 +-
 .../java/org/kohsuke/github/GHCompare.java    |   2 +-
 .../github/GHExternalGroupIterable.java       |   6 +-
 .../org/kohsuke/github/GHSearchBuilder.java   |   2 +-
 .../github/GHWorkflowJobsIterable.java        |   2 +-
 .../github/GHWorkflowRunsIterable.java        |   2 +-
 .../kohsuke/github/GHWorkflowsIterable.java   |   2 +-
 .../org/kohsuke/github/PagedIterable.java     |   6 +-
 .../org/kohsuke/github/PagedIterator.java     |   4 +-
 .../kohsuke/github/PagedSearchIterable.java   |   8 +-
 ...ntIterable.java => PaginatedEndpoint.java} |  92 +++++--------
 ...rator.java => PaginatedEndpointItems.java} |   8 +-
 ...rator.java => PaginatedEndpointPages.java} | 128 +++++++++---------
 .../java/org/kohsuke/github/Requester.java    |   5 +-
 19 files changed, 128 insertions(+), 157 deletions(-)
 rename src/main/java/org/kohsuke/github/{GitHubEndpointIterable.java => PaginatedEndpoint.java} (66%)
 rename src/main/java/org/kohsuke/github/{GitHubPageItemIterator.java => PaginatedEndpointItems.java} (93%)
 rename src/main/java/org/kohsuke/github/{GitHubEndpointPageIterator.java => PaginatedEndpointPages.java} (92%)

diff --git a/src/main/java/org/kohsuke/github/GHAppInstallation.java b/src/main/java/org/kohsuke/github/GHAppInstallation.java
index ec343924e1..cbfbef2df0 100644
--- a/src/main/java/org/kohsuke/github/GHAppInstallation.java
+++ b/src/main/java/org/kohsuke/github/GHAppInstallation.java
@@ -267,7 +267,7 @@ public PagedSearchIterable<GHRepository> listRepositories() {
 
         request = root().createRequest().withUrlPath("/installation/repositories").build();
 
-        return new PagedSearchIterable<>(new GitHubEndpointIterable<>(root()
+        return new PagedSearchIterable<>(new PaginatedEndpoint<>(root()
                 .getClient(), request, GHAppInstallationRepositoryResult.class, GHRepository.class, null));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java b/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
index d089e24e49..88e9281775 100644
--- a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
@@ -16,7 +16,7 @@ class GHAppInstallationsIterable extends PagedIterable<GHAppInstallation> {
      *            the root
      */
     public GHAppInstallationsIterable(GitHub root) {
-        super(new GitHubEndpointIterable<>(root.getClient(),
+        super(new PaginatedEndpoint<>(root.getClient(),
                 root.createRequest().withUrlPath(APP_INSTALLATIONS_URL).build(),
                 GHAppInstallationsPage.class,
                 GHAppInstallation.class,
diff --git a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java b/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
index b16678ac28..bf33db5845 100644
--- a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
@@ -15,7 +15,7 @@ class GHArtifactsIterable extends PagedIterable<GHArtifact> {
      *            the request builder
      */
     public GHArtifactsIterable(GHRepository owner, GitHubRequest.Builder<?> requestBuilder) {
-        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+        super(new PaginatedEndpoint<>(owner.root().getClient(),
                 requestBuilder.build(),
                 GHArtifactsPage.class,
                 GHArtifact.class,
diff --git a/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java b/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java
index 875b285287..0eaa1e52ea 100644
--- a/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java
+++ b/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java
@@ -39,7 +39,7 @@ public PagedSearchIterable<GHRepository> listRepositories() {
 
         request = root().createRequest().withUrlPath("/installation/repositories").build();
 
-        return new PagedSearchIterable<>(new GitHubEndpointIterable<>(root()
+        return new PagedSearchIterable<>(new PaginatedEndpoint<>(root()
                 .getClient(), request, GHAuthenticatedAppInstallationRepositoryResult.class, GHRepository.class, null));
     }
 
diff --git a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java b/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
index 5f75eccc4e..6186b96b72 100644
--- a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
@@ -14,7 +14,7 @@ class GHCheckRunsIterable extends PagedIterable<GHCheckRun> {
      *            the request
      */
     public GHCheckRunsIterable(GHRepository owner, GitHubRequest request) {
-        super(new GitHubEndpointIterable<>(owner.root()
+        super(new PaginatedEndpoint<>(owner.root()
                 .getClient(), request, GHCheckRunsPage.class, GHCheckRun.class, item -> item.wrap(owner)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHCommitFileIterable.java b/src/main/java/org/kohsuke/github/GHCommitFileIterable.java
index 1d83abc4a1..9ec907010c 100644
--- a/src/main/java/org/kohsuke/github/GHCommitFileIterable.java
+++ b/src/main/java/org/kohsuke/github/GHCommitFileIterable.java
@@ -17,19 +17,19 @@ class GHCommitFileIterable extends PagedIterable<GHCommit.File> {
      */
     private static final int GH_FILE_LIMIT_PER_COMMIT_PAGE = 300;
 
-    private static GitHubEndpointIterable<GHCommitFilesPage, File> createEndpointIterable(GHRepository owner,
+    private static PaginatedEndpoint<GHCommitFilesPage, File> createEndpointIterable(GHRepository owner,
             String sha,
             GHCommit.File[] files) {
-        GitHubEndpointIterable<GHCommitFilesPage, File> iterable;
+        PaginatedEndpoint<GHCommitFilesPage, File> iterable;
         if (files != null && files.length < GH_FILE_LIMIT_PER_COMMIT_PAGE) {
             // create a page iterator that only provides one page
-            iterable = GitHubEndpointIterable.ofSingleton(new GHCommitFilesPage(files));
+            iterable = PaginatedEndpoint.ofSingleton(new GHCommitFilesPage(files));
         } else {
             GitHubRequest request = owner.root()
                     .createRequest()
                     .withUrlPath(owner.getApiTailUrl("commits/" + sha))
                     .build();
-            iterable = new GitHubEndpointIterable<>(owner.root()
+            iterable = new PaginatedEndpoint<>(owner.root()
                     .getClient(), request, GHCommitFilesPage.class, GHCommit.File.class, null);
         }
         return iterable;
diff --git a/src/main/java/org/kohsuke/github/GHCompare.java b/src/main/java/org/kohsuke/github/GHCompare.java
index 52ccb8a922..d9993f54db 100644
--- a/src/main/java/org/kohsuke/github/GHCompare.java
+++ b/src/main/java/org/kohsuke/github/GHCompare.java
@@ -345,7 +345,7 @@ public PagedIterable<Commit> listCommits() {
                     .withPageSize(10);
         } else {
             // if not using paginated commits, adapt the returned commits array
-            return new PagedIterable<>(GitHubEndpointIterable.ofSingleton(this.commits));
+            return new PagedIterable<>(PaginatedEndpoint.ofSingleton(this.commits));
         }
     }
 
diff --git a/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java b/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java
index a4a032bab7..850dbdff4e 100644
--- a/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java
+++ b/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java
@@ -18,14 +18,14 @@ class GHExternalGroupIterable extends PagedIterable<GHExternalGroup> {
      *            the request builder
      */
     GHExternalGroupIterable(final GHOrganization owner, GitHubRequest.Builder<?> requestBuilder) {
-        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+        super(new PaginatedEndpoint<>(owner.root().getClient(),
                 requestBuilder.build(),
                 GHExternalGroupPage.class,
                 GHExternalGroup.class,
                 item -> item.wrapUp(owner)) {
             @NotNull @Override
-            public GitHubEndpointPageIterator<GHExternalGroupPage, GHExternalGroup> pageIterator() {
-                return new GitHubEndpointPageIterator<>(client, pageType, request, pageSize, itemInitializer) {
+            public PaginatedEndpointPages<GHExternalGroupPage, GHExternalGroup> pages() {
+                return new PaginatedEndpointPages<>(client, pageType, request, pageSize, itemInitializer) {
                     @Override
                     public boolean hasNext() {
                         try {
diff --git a/src/main/java/org/kohsuke/github/GHSearchBuilder.java b/src/main/java/org/kohsuke/github/GHSearchBuilder.java
index 3cabb7800c..78a5bda076 100644
--- a/src/main/java/org/kohsuke/github/GHSearchBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHSearchBuilder.java
@@ -77,7 +77,7 @@ PagedSearchIterable<T> list(Consumer<T> itemInitializer) {
 
         req.set("q", StringUtils.join(terms, " "));
         return new PagedSearchIterable<>(
-                new GitHubEndpointIterable<>(root().getClient(), req.build(), receiverType, itemType, itemInitializer));
+                new PaginatedEndpoint<>(root().getClient(), req.build(), receiverType, itemType, itemInitializer));
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
index 38fe89f524..704765f4cb 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
@@ -16,7 +16,7 @@ class GHWorkflowJobsIterable extends PagedIterable<GHWorkflowJob> {
      *            the request
      */
     public GHWorkflowJobsIterable(GHRepository repo, GitHubRequest request) {
-        super(new GitHubEndpointIterable<>(repo.root()
+        super(new PaginatedEndpoint<>(repo.root()
                 .getClient(), request, GHWorkflowJobsPage.class, GHWorkflowJob.class, item -> item.wrapUp(repo)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
index 532d3e6097..f9bb2f2728 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
@@ -14,7 +14,7 @@ class GHWorkflowRunsIterable extends PagedIterable<GHWorkflowRun> {
      *            the request builder
      */
     public GHWorkflowRunsIterable(GHRepository owner, GitHubRequest.Builder<?> requestBuilder) {
-        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+        super(new PaginatedEndpoint<>(owner.root().getClient(),
                 requestBuilder.build(),
                 GHWorkflowRunsPage.class,
                 GHWorkflowRun.class,
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
index 265d1700f5..034c0201f8 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
@@ -13,7 +13,7 @@ class GHWorkflowsIterable extends PagedIterable<GHWorkflow> {
      *            the owner
      */
     public GHWorkflowsIterable(GHRepository owner) {
-        super(new GitHubEndpointIterable<>(owner.root().getClient(),
+        super(new PaginatedEndpoint<>(owner.root().getClient(),
                 owner.root().createRequest().withUrlPath(owner.getApiTailUrl("actions/workflows")).build(),
                 GHWorkflowsPage.class,
                 GHWorkflow.class,
diff --git a/src/main/java/org/kohsuke/github/PagedIterable.java b/src/main/java/org/kohsuke/github/PagedIterable.java
index e598832361..951cad5567 100644
--- a/src/main/java/org/kohsuke/github/PagedIterable.java
+++ b/src/main/java/org/kohsuke/github/PagedIterable.java
@@ -17,12 +17,12 @@
  */
 public class PagedIterable<T> implements Iterable<T> {
 
-    private final GitHubEndpointIterable<?, T> paginatedEndpoint;
+    private final PaginatedEndpoint<?, T> paginatedEndpoint;
 
     /**
      * Instantiates a new git hub page contents iterable.
      */
-    PagedIterable(GitHubEndpointIterable<?, T> paginatedEndpoint) {
+    PagedIterable(PaginatedEndpoint<?, T> paginatedEndpoint) {
         this.paginatedEndpoint = paginatedEndpoint;
     }
 
@@ -32,7 +32,7 @@ public PagedIterator<T> _iterator(int pageSize) {
 
     @Nonnull
     public final PagedIterator<T> iterator() {
-        return new PagedIterator<>(paginatedEndpoint.itemIterator());
+        return new PagedIterator<>(paginatedEndpoint.items());
     }
 
     @Nonnull
diff --git a/src/main/java/org/kohsuke/github/PagedIterator.java b/src/main/java/org/kohsuke/github/PagedIterator.java
index d4c334692d..a85bf7128d 100644
--- a/src/main/java/org/kohsuke/github/PagedIterator.java
+++ b/src/main/java/org/kohsuke/github/PagedIterator.java
@@ -19,7 +19,7 @@
  */
 public class PagedIterator<T> implements Iterator<T> {
 
-    private final GitHubPageItemIterator<?, T> endpointIterator;
+    private final PaginatedEndpointItems<?, T> endpointIterator;
 
     /**
      * Instantiates a new paged iterator.
@@ -27,7 +27,7 @@ public class PagedIterator<T> implements Iterator<T> {
      * @param endpointIterator
      *            the base
      */
-    PagedIterator(GitHubPageItemIterator<?, T> endpointIterator) {
+    PagedIterator(PaginatedEndpointItems<?, T> endpointIterator) {
         this.endpointIterator = endpointIterator;
     }
 
diff --git a/src/main/java/org/kohsuke/github/PagedSearchIterable.java b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
index 3f7d5af54d..722d0c1bc8 100644
--- a/src/main/java/org/kohsuke/github/PagedSearchIterable.java
+++ b/src/main/java/org/kohsuke/github/PagedSearchIterable.java
@@ -16,12 +16,12 @@
         justification = "Constructed by JSON API")
 public class PagedSearchIterable<T> extends PagedIterable<T> {
 
-    private final GitHubEndpointIterable<? extends SearchResult<T>, T> paginatedEndpoint;
+    private final PaginatedEndpoint<? extends SearchResult<T>, T> paginatedEndpoint;
 
     /**
      * Instantiates a new git hub page contents iterable.
      */
-    <Result extends SearchResult<T>> PagedSearchIterable(GitHubEndpointIterable<Result, T> paginatedEndpoint) {
+    <Result extends SearchResult<T>> PagedSearchIterable(PaginatedEndpoint<Result, T> paginatedEndpoint) {
         super(paginatedEndpoint);
         this.paginatedEndpoint = paginatedEndpoint;
     }
@@ -33,7 +33,7 @@ <Result extends SearchResult<T>> PagedSearchIterable(GitHubEndpointIterable<Resu
      */
     public int getTotalCount() {
         // populate();
-        return paginatedEndpoint.pageIterator().peek().totalCount;
+        return paginatedEndpoint.pages().peek().totalCount;
     }
 
     /**
@@ -43,6 +43,6 @@ public int getTotalCount() {
      */
     public boolean isIncomplete() {
         // populate();
-        return paginatedEndpoint.pageIterator().peek().incompleteResults;
+        return paginatedEndpoint.pages().peek().incompleteResults;
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java b/src/main/java/org/kohsuke/github/PaginatedEndpoint.java
similarity index 66%
rename from src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
rename to src/main/java/org/kohsuke/github/PaginatedEndpoint.java
index 6f0676d0c5..9d69ae2fdf 100644
--- a/src/main/java/org/kohsuke/github/GitHubEndpointIterable.java
+++ b/src/main/java/org/kohsuke/github/PaginatedEndpoint.java
@@ -10,21 +10,21 @@
 import javax.annotation.Nonnull;
 
 /**
- * {@link GitHubEndpointIterable} implementation that take a {@link Consumer} that initializes all the items on each
- * page as they are retrieved.
+ * {@link PaginatedEndpoint} implementation that take a {@link Consumer} that initializes all the items on each page as
+ * they are retrieved.
  *
- * {@link GitHubEndpointIterable} is immutable and thread-safe, but the iterator returned from {@link #iterator()} is
- * not. Any one instance of iterator should only be called from a single thread.
+ * {@link PaginatedEndpoint} is immutable and thread-safe, but the iterator returned from {@link #iterator()} is not.
+ * Any one instance of iterator should only be called from a single thread.
  *
  * @author Liam Newman
  * @param <Item>
  *            the type of items on each page
  */
-class GitHubEndpointIterable<Page extends GitHubPage<Item>, Item> implements Iterable<Item> {
+class PaginatedEndpoint<Page extends GitHubPage<Item>, Item> implements Iterable<Item> {
 
-    private static class ArrayIterable<I> extends GitHubEndpointIterable<GitHubPage<I>, I> {
+    private static class ArrayIterable<I> extends PaginatedEndpoint<GitHubPage<I>, I> {
 
-        private class ArrayIterator extends GitHubEndpointPageIterator<GitHubPage<I>, I> {
+        private class ArrayIterator extends PaginatedEndpointPages<GitHubPage<I>, I> {
 
             ArrayIterator(GitHubClient client,
                     Class<GitHubPage<I>> pageType,
@@ -58,7 +58,7 @@ private ArrayIterable(GitHubClient client,
         }
 
         @NotNull @Override
-        public GitHubEndpointPageIterator<GitHubPage<I>, I> pageIterator() {
+        public PaginatedEndpointPages<GitHubPage<I>, I> pages() {
             return new ArrayIterator(client, pageType, request, pageSize, itemInitializer);
         }
     }
@@ -91,24 +91,24 @@ public I[] getItems() {
         }
     }
 
-    static <I> GitHubEndpointIterable<GitHubPage<I>, I> ofArrayEndpoint(GitHubClient client,
+    static <I> PaginatedEndpoint<GitHubPage<I>, I> ofArrayEndpoint(GitHubClient client,
             GitHubRequest request,
             Class<I[]> receiverType,
             Consumer<I> itemInitializer) {
         return new ArrayIterable<>(client, request, receiverType, itemInitializer);
     }
 
-    static <I> GitHubEndpointIterable<GitHubPage<I>, I> ofSingleton(I[] array) {
+    static <I> PaginatedEndpoint<GitHubPage<I>, I> ofSingleton(I[] array) {
         return ofSingleton(new GitHubArrayPage<>(array));
     }
 
-    static <P extends GitHubPage<I>, I> GitHubEndpointIterable<P, I> ofSingleton(P page) {
+    static <P extends GitHubPage<I>, I> PaginatedEndpoint<P, I> ofSingleton(P page) {
         Class<I> itemType = (Class<I>) page.getItems().getClass().getComponentType();
-        return new GitHubEndpointIterable<>(null, null, (Class<P>) page.getClass(), itemType, null) {
+        return new PaginatedEndpoint<>(null, null, (Class<P>) page.getClass(), itemType, null) {
             @Nonnull
             @Override
-            public GitHubEndpointPageIterator<P, I> pageIterator() {
-                return GitHubEndpointPageIterator.ofSingleton(page);
+            public PaginatedEndpointPages<P, I> pages() {
+                return PaginatedEndpointPages.ofSingleton(page);
             }
         };
     }
@@ -138,7 +138,7 @@ public GitHubEndpointPageIterator<P, I> pageIterator() {
      * @param itemInitializer
      *            the item initializer
      */
-    GitHubEndpointIterable(GitHubClient client,
+    PaginatedEndpoint(GitHubClient client,
             GitHubRequest request,
             Class<Page> pageType,
             Class<Item> itemType,
@@ -151,13 +151,13 @@ public GitHubEndpointPageIterator<P, I> pageIterator() {
     }
 
     @Nonnull
-    public final GitHubPageItemIterator<Page, Item> itemIterator() {
-        return new GitHubPageItemIterator<>(this.pageIterator());
+    public final PaginatedEndpointItems<Page, Item> items() {
+        return new PaginatedEndpointItems<>(this.pages());
     }
     @Nonnull
     @Override
     public final Iterator<Item> iterator() {
-        return this.itemIterator();
+        return this.items();
     }
 
     /**
@@ -165,8 +165,8 @@ public final Iterator<Item> iterator() {
      * @return
      */
     @Nonnull
-    public GitHubEndpointPageIterator<Page, Item> pageIterator() {
-        return new GitHubEndpointPageIterator<>(client, pageType, request, pageSize, itemInitializer);
+    public PaginatedEndpointPages<Page, Item> pages() {
+        return new PaginatedEndpointPages<>(client, pageType, request, pageSize, itemInitializer);
     }
 
     /**
@@ -178,7 +178,7 @@ public GitHubEndpointPageIterator<Page, Item> pageIterator() {
      */
     @Nonnull
     public final Item[] toArray() throws IOException {
-        return toArray(pageIterator(), itemType);
+        return toList().toArray((Item[]) Array.newInstance(itemType, 0));
     }
 
     /**
@@ -190,7 +190,7 @@ public final Item[] toArray() throws IOException {
      */
     @Nonnull
     public final List<Item> toList() throws IOException {
-        return Collections.unmodifiableList(Arrays.asList(this.toArray()));
+        return Collections.unmodifiableList(toList(pages(), itemType));
     }
 
     /**
@@ -202,7 +202,7 @@ public final List<Item> toList() throws IOException {
      */
     @Nonnull
     public final Set<Item> toSet() throws IOException {
-        return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(this.toArray())));
+        return Collections.unmodifiableSet(new LinkedHashSet<>(toList()));
     }
 
     /**
@@ -215,33 +215,11 @@ public final Set<Item> toSet() throws IOException {
      *            the size
      * @return the paged iterable
      */
-    public final GitHubEndpointIterable<Page, Item> withPageSize(int size) {
+    public final PaginatedEndpoint<Page, Item> withPageSize(int size) {
         this.pageSize = size;
         return this;
     }
 
-    /**
-     * Concatenates a list of arrays into a single array.
-     *
-     * @param pages
-     *            the list of arrays to be concatenated.
-     * @param totalLength
-     *            the total length of the returned array.
-     * @return an array containing all elements from all pages.
-     */
-    @Nonnull
-    private Item[] concatenatePages(List<Item[]> pages, int totalLength) {
-        Item[] result = (Item[]) Array.newInstance(itemType, totalLength);
-
-        int position = 0;
-        for (Item[] page : pages) {
-            final int pageLength = Array.getLength(page);
-            System.arraycopy(page, 0, result, position, pageLength);
-            position += pageLength;
-        }
-        return result;
-    }
-
     /**
      * Eagerly walk {@link PagedIterator} and return the result in an array.
      *
@@ -251,18 +229,14 @@ private Item[] concatenatePages(List<Item[]> pages, int totalLength) {
      * @throws IOException
      *             if an I/O exception occurs.
      */
-    private Item[] toArray(final GitHubEndpointPageIterator<Page, Item> iterator, Class<Item> itemType) throws IOException {
+    private List<Item> toList(final PaginatedEndpointPages<Page, Item> iterator, Class<Item> itemType)
+            throws IOException {
         try {
-            ArrayList<Item[]> pages = new ArrayList<>();
-            int totalSize = 0;
-            Item[] item;
-            while (iterator.hasNext()) {
-                item = iterator.next().getItems();
-                totalSize += Array.getLength(item);
-                pages.add(item);
-            }
-
-            return concatenatePages(pages, totalSize);
+            ArrayList<Item> pageList = new ArrayList<>();
+            iterator.forEachRemaining(page -> {
+                pageList.addAll(Arrays.asList(page.getItems()));
+            });
+            return pageList;
         } catch (GHException e) {
             // if there was an exception inside the iterator it is wrapped as a GHException
             // if the wrapped exception is an IOException, throw that
@@ -284,8 +258,8 @@ private Item[] toArray(final GitHubEndpointPageIterator<Page, Item> iterator, Cl
      */
     @Nonnull
     final GitHubResponse<Item[]> toResponse() throws IOException {
-        GitHubEndpointPageIterator<Page, Item> iterator = pageIterator();
-        Item[] items = toArray(iterator, itemType);
+        PaginatedEndpointPages<Page, Item> iterator = pages();
+        Item[] items = toArray();
         GitHubResponse<Page> lastResponse = iterator.finalResponse();
         return new GitHubResponse<>(lastResponse, items);
     }
diff --git a/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java b/src/main/java/org/kohsuke/github/PaginatedEndpointItems.java
similarity index 93%
rename from src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
rename to src/main/java/org/kohsuke/github/PaginatedEndpointItems.java
index 31869c3dde..305a47be56 100644
--- a/src/main/java/org/kohsuke/github/GitHubPageItemIterator.java
+++ b/src/main/java/org/kohsuke/github/PaginatedEndpointItems.java
@@ -2,12 +2,10 @@
 
 import java.util.*;
 
-import javax.annotation.Nonnull;
-
 /**
  * This class is not thread-safe. Any one instance should only be called from a single thread.
  */
-class GitHubPageItemIterator<Page extends GitHubPage<Item>, Item> implements Iterator<Item> {
+class PaginatedEndpointItems<Page extends GitHubPage<Item>, Item> implements Iterator<Item> {
 
     /**
      * Current batch of items. Each time {@link #next()} is called the next item in this array will be returned. After
@@ -25,9 +23,9 @@ class GitHubPageItemIterator<Page extends GitHubPage<Item>, Item> implements Ite
      */
     private int nextItemIndex;
 
-    private final GitHubEndpointPageIterator<Page, Item> pageIterator;
+    private final PaginatedEndpointPages<Page, Item> pageIterator;
 
-    GitHubPageItemIterator(GitHubEndpointPageIterator<Page, Item> pageIterator) {
+    PaginatedEndpointItems(PaginatedEndpointPages<Page, Item> pageIterator) {
         this.pageIterator = pageIterator;
     }
 
diff --git a/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java b/src/main/java/org/kohsuke/github/PaginatedEndpointPages.java
similarity index 92%
rename from src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
rename to src/main/java/org/kohsuke/github/PaginatedEndpointPages.java
index 5f50835e6e..a075832a30 100644
--- a/src/main/java/org/kohsuke/github/GitHubEndpointPageIterator.java
+++ b/src/main/java/org/kohsuke/github/PaginatedEndpointPages.java
@@ -2,12 +2,13 @@
 
 import org.jetbrains.annotations.NotNull;
 
-import javax.annotation.Nonnull;
 import java.io.IOException;
 import java.net.URL;
 import java.util.NoSuchElementException;
 import java.util.function.Consumer;
 
+import javax.annotation.Nonnull;
+
 /**
  * May be used for any item that has pagination information. Iterates over paginated {@code P} objects (not the items
  * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse}{@code
@@ -23,9 +24,16 @@
  * @param <P>
  *            type of each page (not the items in the page).
  */
-class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> implements java.util.Iterator<P> {
+class PaginatedEndpointPages<P extends GitHubPage<Item>, Item> implements java.util.Iterator<P> {
 
-    protected final Class<P> pageType;
+    static <P extends GitHubPage<Item>, Item> PaginatedEndpointPages<P, Item> ofSingleton(final P page) {
+        return new PaginatedEndpointPages<>(page);
+    }
+    /**
+     * When done iterating over pages, it is on rare occasions useful to be able to get information from the final
+     * response that was retrieved.
+     */
+    private GitHubResponse<P> finalResponse = null;
     private final Consumer<Item> itemInitializer;
     /**
      * The page that will be returned when {@link #next()} is called.
@@ -38,11 +46,6 @@ class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> implements ja
      * </p>
      */
     private P next;
-    /**
-     * When done iterating over pages, it is on rare occasions useful to be able to get information from the final
-     * response that was retrieved.
-     */
-    private GitHubResponse<P> finalResponse = null;
 
     protected final GitHubClient client;
 
@@ -52,12 +55,14 @@ class GitHubEndpointPageIterator<P extends GitHubPage<Item>, Item> implements ja
      */
     protected GitHubRequest nextRequest;
 
-    private GitHubEndpointPageIterator(P page) {
-        this(null, (Class<P>)page.getClass(), null, 0, null);
+    protected final Class<P> pageType;
+
+    private PaginatedEndpointPages(P page) {
+        this(null, (Class<P>) page.getClass(), null, 0, null);
         this.next = page;
     }
 
-    GitHubEndpointPageIterator(GitHubClient client,
+    PaginatedEndpointPages(GitHubClient client,
             Class<P> pageType,
             GitHubRequest request,
             int pageSize,
@@ -78,10 +83,6 @@ private GitHubEndpointPageIterator(P page) {
         this.nextRequest = request;
     }
 
-    static <P extends GitHubPage<Item>, Item> GitHubEndpointPageIterator<P, Item> ofSingleton(final P page) {
-        return new GitHubEndpointPageIterator<>(page);
-    }
-
     /**
      * On rare occasions the final response from iterating is needed.
      *
@@ -94,6 +95,53 @@ public GitHubResponse<P> finalResponse() {
         return finalResponse;
     }
 
+    /**
+     * {@inheritDoc}
+     */
+    public boolean hasNext() {
+        return peek() != null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Nonnull
+    public P next() {
+        P result = peek();
+        if (result == null)
+            throw new NoSuchElementException();
+        next = null;
+        return result;
+    }
+
+    /**
+     *
+     * @return
+     */
+    public P peek() {
+        if (next == null) {
+            P result = fetchNext();
+            if (result != null) {
+                next = result;
+                initializeItems();
+            }
+        }
+        return next;
+    }
+
+    /**
+     * This method initializes items with local data after they are fetched. It is up to the implementer to decide what
+     * local data to apply.
+     *
+     */
+    private void initializeItems() {
+        if (itemInitializer != null) {
+            for (Item item : next.getItems()) {
+                itemInitializer.accept(item);
+            }
+        }
+    }
+
     /**
      * Locate the next page from the pagination "Link" tag.
      */
@@ -122,8 +170,7 @@ private void updateNextRequest(GitHubResponse<P> nextResponse) {
      * Fetch is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it is
      * needed.
      * <p>
-     * If {@link #next} is not {@code null}, no further action is needed.
-     * If {@link #next} is {@code null} and
+     * If {@link #next} is not {@code null}, no further action is needed. If {@link #next} is {@code null} and
      * {@link #nextRequest} is {@code null}, there are no more pages to fetch.
      * </p>
      * <p>
@@ -156,51 +203,4 @@ protected P fetchNext() {
         return client.sendRequest(nextRequest,
                 (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, pageType));
     }
-
-    /**
-     * {@inheritDoc}
-     */
-    public boolean hasNext() {
-        return peek() != null;
-    }
-
-    /**
-     * {@inheritDoc}
-     */
-    @Nonnull
-    public P next() {
-        P result = peek();
-        if (result == null)
-            throw new NoSuchElementException();
-        next = null;
-        return result;
-    }
-
-    /**
-     *
-     * @return
-     */
-    public P peek() {
-        if (next == null) {
-            P result = fetchNext();
-            if (result != null) {
-                next = result;
-                initializeItems();
-            }
-        }
-        return next;
-    }
-
-    /**
-     * This method initializes items with local data after they are fetched. It is up to the implementer to decide what
-     * local data to apply.
-     *
-     */
-    private void initializeItems() {
-        if (itemInitializer != null) {
-            for (Item item : next.getItems()) {
-                itemInitializer.accept(item);
-            }
-        }
-    }
 }
diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java
index e6fd73168d..2a3d80140b 100644
--- a/src/main/java/org/kohsuke/github/Requester.java
+++ b/src/main/java/org/kohsuke/github/Requester.java
@@ -203,7 +203,7 @@ public <P extends GitHubPage<R>, R> PagedIterable<R> toIterable(Class<P> pageTyp
             Class<R> itemType,
             Consumer<R> itemInitializer) {
         GitHubRequest request = build();
-        return new PagedIterable<>(new GitHubEndpointIterable<>(client, request, pageType, itemType, itemInitializer));
+        return new PagedIterable<>(new PaginatedEndpoint<>(client, request, pageType, itemType, itemInitializer));
     }
 
     /**
@@ -223,8 +223,7 @@ public <P extends GitHubPage<R>, R> PagedIterable<R> toIterable(Class<P> pageTyp
      */
     public <R> PagedIterable<R> toIterable(Class<R[]> receiverType, Consumer<R> itemInitializer) {
         GitHubRequest request = build();
-        return new PagedIterable<>(
-                GitHubEndpointIterable.ofArrayEndpoint(client, request, receiverType, itemInitializer));
+        return new PagedIterable<>(PaginatedEndpoint.ofArrayEndpoint(client, request, receiverType, itemInitializer));
     }
 
 }

From 01f8dfacb6ed101cb185e9c5b682d3b94a1bca97 Mon Sep 17 00:00:00 2001
From: Liam Newman <bitwiseman@gmail.com>
Date: Mon, 5 May 2025 09:33:55 -0700
Subject: [PATCH 4/4] Remove extraneous classes

---
 .../github/GHAppInstallationsIterable.java    | 25 -------------------
 .../kohsuke/github/GHArtifactsIterable.java   | 24 ------------------
 .../kohsuke/github/GHCheckRunsIterable.java   | 20 ---------------
 .../java/org/kohsuke/github/GHMyself.java     |  9 ++++++-
 .../java/org/kohsuke/github/GHRepository.java | 18 ++++++++++---
 .../java/org/kohsuke/github/GHWorkflow.java   |  6 ++++-
 .../github/GHWorkflowJobQueryBuilder.java     |  3 ++-
 .../github/GHWorkflowJobsIterable.java        | 22 ----------------
 .../org/kohsuke/github/GHWorkflowRun.java     |  6 ++++-
 .../github/GHWorkflowRunQueryBuilder.java     |  6 ++++-
 .../github/GHWorkflowRunsIterable.java        | 23 -----------------
 .../kohsuke/github/GHWorkflowsIterable.java   | 22 ----------------
 12 files changed, 39 insertions(+), 145 deletions(-)
 delete mode 100644 src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
 delete mode 100644 src/main/java/org/kohsuke/github/GHArtifactsIterable.java
 delete mode 100644 src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
 delete mode 100644 src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
 delete mode 100644 src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
 delete mode 100644 src/main/java/org/kohsuke/github/GHWorkflowsIterable.java

diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java b/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
deleted file mode 100644
index 88e9281775..0000000000
--- a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package org.kohsuke.github;
-
-// TODO: Auto-generated Javadoc
-/**
- * Iterable for GHAppInstallation listing.
- */
-class GHAppInstallationsIterable extends PagedIterable<GHAppInstallation> {
-
-    /** The Constant APP_INSTALLATIONS_URL. */
-    public static final String APP_INSTALLATIONS_URL = "/user/installations";
-
-    /**
-     * Instantiates a new GH app installations iterable.
-     *
-     * @param root
-     *            the root
-     */
-    public GHAppInstallationsIterable(GitHub root) {
-        super(new PaginatedEndpoint<>(root.getClient(),
-                root.createRequest().withUrlPath(APP_INSTALLATIONS_URL).build(),
-                GHAppInstallationsPage.class,
-                GHAppInstallation.class,
-                null));
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java b/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
deleted file mode 100644
index bf33db5845..0000000000
--- a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package org.kohsuke.github;
-
-// TODO: Auto-generated Javadoc
-/**
- * Iterable for artifacts listing.
- */
-class GHArtifactsIterable extends PagedIterable<GHArtifact> {
-
-    /**
-     * Instantiates a new GH artifacts iterable.
-     *
-     * @param owner
-     *            the owner
-     * @param requestBuilder
-     *            the request builder
-     */
-    public GHArtifactsIterable(GHRepository owner, GitHubRequest.Builder<?> requestBuilder) {
-        super(new PaginatedEndpoint<>(owner.root().getClient(),
-                requestBuilder.build(),
-                GHArtifactsPage.class,
-                GHArtifact.class,
-                item -> item.wrapUp(owner)));
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java b/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
deleted file mode 100644
index 6186b96b72..0000000000
--- a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.kohsuke.github;
-
-// TODO: Auto-generated Javadoc
-/**
- * Iterable for check-runs listing.
- */
-class GHCheckRunsIterable extends PagedIterable<GHCheckRun> {
-    /**
-     * Instantiates a new GH check runs iterable.
-     *
-     * @param owner
-     *            the owner
-     * @param request
-     *            the request
-     */
-    public GHCheckRunsIterable(GHRepository owner, GitHubRequest request) {
-        super(new PaginatedEndpoint<>(owner.root()
-                .getClient(), request, GHCheckRunsPage.class, GHCheckRun.class, item -> item.wrap(owner)));
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/GHMyself.java b/src/main/java/org/kohsuke/github/GHMyself.java
index 05e52cd5ac..ba0a8ae410 100644
--- a/src/main/java/org/kohsuke/github/GHMyself.java
+++ b/src/main/java/org/kohsuke/github/GHMyself.java
@@ -38,6 +38,9 @@ public enum RepositoryListFilter {
         PUBLIC;
     }
 
+    /** The Constant APP_INSTALLATIONS_URL. */
+    public static final String APP_INSTALLATIONS_URL = "/user/installations";
+
     /**
      * Create default GHMyself instance
      */
@@ -110,7 +113,11 @@ public synchronized Map<String, GHRepository> getAllRepositories() {
      *      app installations accessible to the user access token</a>
      */
     public PagedIterable<GHAppInstallation> getAppInstallations() {
-        return new GHAppInstallationsIterable(root());
+        return new PagedIterable<>(new PaginatedEndpoint<>(root().getClient(),
+                root().createRequest().withUrlPath(APP_INSTALLATIONS_URL).build(),
+                GHAppInstallationsPage.class,
+                GHAppInstallation.class,
+                null));
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java
index 547a663ad8..f19a7bd7d3 100644
--- a/src/main/java/org/kohsuke/github/GHRepository.java
+++ b/src/main/java/org/kohsuke/github/GHRepository.java
@@ -1192,7 +1192,8 @@ public PagedIterable<GHCheckRun> getCheckRuns(String ref) {
         GitHubRequest request = root().createRequest()
                 .withUrlPath(String.format("/repos/%s/%s/commits/%s/check-runs", getOwnerName(), name, ref))
                 .build();
-        return new GHCheckRunsIterable(this, request);
+        return new PagedIterable<>(new PaginatedEndpoint<>(root()
+                .getClient(), request, GHCheckRunsPage.class, GHCheckRun.class, item -> item.wrap(this)));
     }
 
     /**
@@ -1211,7 +1212,8 @@ public PagedIterable<GHCheckRun> getCheckRuns(String ref, Map<String, Object> pa
                 .withUrlPath(String.format("/repos/%s/%s/commits/%s/check-runs", getOwnerName(), name, ref))
                 .with(params)
                 .build();
-        return new GHCheckRunsIterable(this, request);
+        return new PagedIterable<>(new PaginatedEndpoint<>(root()
+                .getClient(), request, GHCheckRunsPage.class, GHCheckRun.class, item -> item.wrap(this)));
     }
 
     /**
@@ -2548,7 +2550,11 @@ public boolean isVulnerabilityAlertsEnabled() throws IOException {
      * @return the paged iterable
      */
     public PagedIterable<GHArtifact> listArtifacts() {
-        return new GHArtifactsIterable(this, root().createRequest().withUrlPath(getApiTailUrl("actions/artifacts")));
+        return new PagedIterable<>(new PaginatedEndpoint<>(this.root().getClient(),
+                root().createRequest().withUrlPath(getApiTailUrl("actions/artifacts")).build(),
+                GHArtifactsPage.class,
+                GHArtifact.class,
+                item -> item.wrapUp(this)));
     }
 
     /**
@@ -2956,7 +2962,11 @@ public List<String> listTopics() throws IOException {
      * @return the paged iterable
      */
     public PagedIterable<GHWorkflow> listWorkflows() {
-        return new GHWorkflowsIterable(this);
+        return new PagedIterable<>(new PaginatedEndpoint<>(root().getClient(),
+                root().createRequest().withUrlPath(getApiTailUrl("actions/workflows")).build(),
+                GHWorkflowsPage.class,
+                GHWorkflow.class,
+                item -> item.wrapUp(this)));
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHWorkflow.java b/src/main/java/org/kohsuke/github/GHWorkflow.java
index dff9ffdc3d..2e99fcd826 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflow.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflow.java
@@ -153,7 +153,11 @@ public String getState() {
      * @return the paged iterable
      */
     public PagedIterable<GHWorkflowRun> listRuns() {
-        return new GHWorkflowRunsIterable(owner, root().createRequest().withUrlPath(getApiRoute(), "runs"));
+        return new PagedIterable<>(new PaginatedEndpoint<>(owner.root().getClient(),
+                root().createRequest().withUrlPath(getApiRoute(), "runs").build(),
+                GHWorkflowRunsPage.class,
+                GHWorkflowRun.class,
+                item -> item.wrapUp(owner)));
     }
 
     private String getApiRoute() {
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java b/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java
index 9f011e9612..7556737e03 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java
@@ -48,6 +48,7 @@ public GHWorkflowJobQueryBuilder latest() {
      */
     @Override
     public PagedIterable<GHWorkflowJob> list() {
-        return new GHWorkflowJobsIterable(repo, req.build());
+        return new PagedIterable<>(new PaginatedEndpoint<>(repo.root()
+                .getClient(), req.build(), GHWorkflowJobsPage.class, GHWorkflowJob.class, item -> item.wrapUp(repo)));
     }
 }
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
deleted file mode 100644
index 704765f4cb..0000000000
--- a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.kohsuke.github;
-
-// TODO: Auto-generated Javadoc
-/**
- * Iterable for workflow run jobs listing.
- */
-class GHWorkflowJobsIterable extends PagedIterable<GHWorkflowJob> {
-    private GHWorkflowJobsPage result;
-
-    /**
-     * Instantiates a new GH workflow jobs iterable.
-     *
-     * @param repo
-     *            the repo
-     * @param request
-     *            the request
-     */
-    public GHWorkflowJobsIterable(GHRepository repo, GitHubRequest request) {
-        super(new PaginatedEndpoint<>(repo.root()
-                .getClient(), request, GHWorkflowJobsPage.class, GHWorkflowJob.class, item -> item.wrapUp(repo)));
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRun.java b/src/main/java/org/kohsuke/github/GHWorkflowRun.java
index 7e25b29b5f..090720bd8f 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowRun.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowRun.java
@@ -558,7 +558,11 @@ public PagedIterable<GHWorkflowJob> listAllJobs() {
      * @return the paged iterable
      */
     public PagedIterable<GHArtifact> listArtifacts() {
-        return new GHArtifactsIterable(owner, root().createRequest().withUrlPath(getApiRoute(), "artifacts"));
+        return new PagedIterable<>(new PaginatedEndpoint<>(owner.root().getClient(),
+                root().createRequest().withUrlPath(getApiRoute(), "artifacts").build(),
+                GHArtifactsPage.class,
+                GHArtifact.class,
+                item -> item.wrapUp(owner)));
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java b/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java
index 105dd77a84..60c891e69b 100644
--- a/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java
@@ -133,7 +133,11 @@ public GHWorkflowRunQueryBuilder headSha(String headSha) {
      */
     @Override
     public PagedIterable<GHWorkflowRun> list() {
-        return new GHWorkflowRunsIterable(repo, req.withUrlPath(repo.getApiTailUrl("actions/runs")));
+        return new PagedIterable<>(new PaginatedEndpoint<>(repo.root().getClient(),
+                req.withUrlPath(repo.getApiTailUrl("actions/runs")).build(),
+                GHWorkflowRunsPage.class,
+                GHWorkflowRun.class,
+                item -> item.wrapUp(repo)));
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
deleted file mode 100644
index f9bb2f2728..0000000000
--- a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.kohsuke.github;
-
-// TODO: Auto-generated Javadoc
-/**
- * Iterable for workflow runs listing.
- */
-class GHWorkflowRunsIterable extends PagedIterable<GHWorkflowRun> {
-    /**
-     * Instantiates a new GH workflow runs iterable.
-     *
-     * @param owner
-     *            the owner
-     * @param requestBuilder
-     *            the request builder
-     */
-    public GHWorkflowRunsIterable(GHRepository owner, GitHubRequest.Builder<?> requestBuilder) {
-        super(new PaginatedEndpoint<>(owner.root().getClient(),
-                requestBuilder.build(),
-                GHWorkflowRunsPage.class,
-                GHWorkflowRun.class,
-                item -> item.wrapUp(owner)));
-    }
-}
diff --git a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
deleted file mode 100644
index 034c0201f8..0000000000
--- a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package org.kohsuke.github;
-
-// TODO: Auto-generated Javadoc
-/**
- * Iterable for workflows listing.
- */
-class GHWorkflowsIterable extends PagedIterable<GHWorkflow> {
-
-    /**
-     * Instantiates a new GH workflows iterable.
-     *
-     * @param owner
-     *            the owner
-     */
-    public GHWorkflowsIterable(GHRepository owner) {
-        super(new PaginatedEndpoint<>(owner.root().getClient(),
-                owner.root().createRequest().withUrlPath(owner.getApiTailUrl("actions/workflows")).build(),
-                GHWorkflowsPage.class,
-                GHWorkflow.class,
-                item -> item.wrapUp(owner)));
-    }
-}