diff --git a/src/Resources/PullRequestResource.php b/src/Resources/PullRequestResource.php index ab97e12..7731e13 100644 --- a/src/Resources/PullRequestResource.php +++ b/src/Resources/PullRequestResource.php @@ -36,6 +36,37 @@ public function all(string $owner, string $repo, array $parameters = []): array return $response->dto(); } + public function paginate(string $owner, string $repo, array $parameters = []): array + { + $page = 1; + $allPrs = []; + $maxPages = 1000; // Prevent infinite loops + + do { + if ($page > $maxPages) { + throw new \RuntimeException("Maximum page limit ($maxPages) exceeded during pagination"); + } + + $response = $this->github()->connector()->send(new Index("{$owner}/{$repo}", array_merge($parameters, [ + 'page' => $page, + ]))); + + $prs = $response->dto(); + + if (! empty($prs)) { + $allPrs = array_merge($allPrs, $prs); + } + + // Check if there are more pages by examining the Link header + $linkHeader = $response->header('Link'); + $hasNextPage = $linkHeader && str_contains($linkHeader, 'rel="next"'); + + $page++; + } while ($hasNextPage && ! empty($prs)); + + return $allPrs; + } + public function get(string $owner, string $repo, int $number): PullRequestDTO { $response = $this->github()->connector()->send(new Get("{$owner}/{$repo}", $number)); diff --git a/tests/PullRequestsTest.php b/tests/PullRequestsTest.php index 5556f33..0cda5e0 100644 --- a/tests/PullRequestsTest.php +++ b/tests/PullRequestsTest.php @@ -268,3 +268,260 @@ ->and($comment->body)->toBe('Consider using a different approach here'); }); }); + +describe('manual pagination', function () { + beforeEach(function () { + config(['github-client.token' => 'fake-token']); + }); + + it('fetches all pull requests across multiple pages', function () { + $page1Response = MockResponse::make([ + [ + 'id' => 1, + 'number' => 1, + 'state' => 'open', + 'title' => 'PR 1', + 'body' => '', + 'html_url' => 'https://github.com/test/repo/pull/1', + 'diff_url' => 'https://github.com/test/repo/pull/1.diff', + 'patch_url' => 'https://github.com/test/repo/pull/1.patch', + 'base' => ['ref' => 'main'], + 'head' => ['ref' => 'feature-1'], + 'draft' => false, + 'merged' => false, + 'merged_at' => null, + 'merge_commit_sha' => null, + 'comments' => 0, + 'review_comments' => 0, + 'commits' => 1, + 'additions' => 10, + 'deletions' => 5, + 'changed_files' => 2, + 'user' => $this->createMockUserData('testuser', 1), + 'merged_by' => null, + 'created_at' => '2024-01-01T00:00:00Z', + 'updated_at' => '2024-01-01T00:00:00Z', + 'closed_at' => null, + ], + [ + 'id' => 2, + 'number' => 2, + 'state' => 'open', + 'title' => 'PR 2', + 'body' => '', + 'html_url' => 'https://github.com/test/repo/pull/2', + 'diff_url' => 'https://github.com/test/repo/pull/2.diff', + 'patch_url' => 'https://github.com/test/repo/pull/2.patch', + 'base' => ['ref' => 'main'], + 'head' => ['ref' => 'feature-2'], + 'draft' => false, + 'merged' => false, + 'merged_at' => null, + 'merge_commit_sha' => null, + 'comments' => 0, + 'review_comments' => 0, + 'commits' => 1, + 'additions' => 10, + 'deletions' => 5, + 'changed_files' => 2, + 'user' => $this->createMockUserData('testuser', 1), + 'merged_by' => null, + 'created_at' => '2024-01-01T00:00:00Z', + 'updated_at' => '2024-01-01T00:00:00Z', + 'closed_at' => null, + ], + ], 200, [ + 'Link' => '; rel="next", ; rel="last"', + ]); + + $page2Response = MockResponse::make([ + [ + 'id' => 3, + 'number' => 3, + 'state' => 'open', + 'title' => 'PR 3', + 'body' => '', + 'html_url' => 'https://github.com/test/repo/pull/3', + 'diff_url' => 'https://github.com/test/repo/pull/3.diff', + 'patch_url' => 'https://github.com/test/repo/pull/3.patch', + 'base' => ['ref' => 'main'], + 'head' => ['ref' => 'feature-3'], + 'draft' => false, + 'merged' => false, + 'merged_at' => null, + 'merge_commit_sha' => null, + 'comments' => 0, + 'review_comments' => 0, + 'commits' => 1, + 'additions' => 10, + 'deletions' => 5, + 'changed_files' => 2, + 'user' => $this->createMockUserData('testuser', 1), + 'merged_by' => null, + 'created_at' => '2024-01-01T00:00:00Z', + 'updated_at' => '2024-01-01T00:00:00Z', + 'closed_at' => null, + ], + ], 200, [ + 'Link' => '; rel="first", ; rel="prev"', + ]); + + $mockClient = new MockClient(); + $mockClient->addResponse($page1Response); + $mockClient->addResponse($page2Response); + + Github::connector()->withMockClient($mockClient); + + $allPrs = Github::pullRequests()->paginate('test', 'repo'); + + expect($allPrs) + ->toBeArray() + ->toHaveCount(3) + ->and($allPrs[0]->title)->toBe('PR 1') + ->and($allPrs[1]->title)->toBe('PR 2') + ->and($allPrs[2]->title)->toBe('PR 3'); + }); + + it('stops pagination when no more pages available', function () { + $singlePageResponse = MockResponse::make([ + [ + 'id' => 1, + 'number' => 1, + 'state' => 'open', + 'title' => 'PR 1', + 'body' => '', + 'html_url' => 'https://github.com/test/repo/pull/1', + 'diff_url' => 'https://github.com/test/repo/pull/1.diff', + 'patch_url' => 'https://github.com/test/repo/pull/1.patch', + 'base' => ['ref' => 'main'], + 'head' => ['ref' => 'feature-1'], + 'draft' => false, + 'merged' => false, + 'merged_at' => null, + 'merge_commit_sha' => null, + 'comments' => 0, + 'review_comments' => 0, + 'commits' => 1, + 'additions' => 10, + 'deletions' => 5, + 'changed_files' => 2, + 'user' => $this->createMockUserData('testuser', 1), + 'merged_by' => null, + 'created_at' => '2024-01-01T00:00:00Z', + 'updated_at' => '2024-01-01T00:00:00Z', + 'closed_at' => null, + ], + ], 200); + + Github::connector()->withMockClient(new MockClient([ + '*' => $singlePageResponse, + ])); + + $allPrs = Github::pullRequests()->paginate('test', 'repo'); + + expect($allPrs) + ->toBeArray() + ->toHaveCount(1) + ->and($allPrs[0]->title)->toBe('PR 1'); + }); + + it('handles empty pull request list', function () { + Github::connector()->withMockClient(new MockClient([ + '*' => MockResponse::make([], 200), + ])); + + $allPrs = Github::pullRequests()->paginate('test', 'repo'); + + expect($allPrs) + ->toBeArray() + ->toHaveCount(0); + }); + + it('respects filtering parameters during pagination', function () { + $openPrsResponse = MockResponse::make([ + [ + 'id' => 1, + 'number' => 1, + 'state' => 'open', + 'title' => 'Open PR', + 'body' => '', + 'html_url' => 'https://github.com/test/repo/pull/1', + 'diff_url' => 'https://github.com/test/repo/pull/1.diff', + 'patch_url' => 'https://github.com/test/repo/pull/1.patch', + 'base' => ['ref' => 'main'], + 'head' => ['ref' => 'feature-1'], + 'draft' => false, + 'merged' => false, + 'merged_at' => null, + 'merge_commit_sha' => null, + 'comments' => 0, + 'review_comments' => 0, + 'commits' => 1, + 'additions' => 10, + 'deletions' => 5, + 'changed_files' => 2, + 'user' => $this->createMockUserData('testuser', 1), + 'merged_by' => null, + 'created_at' => '2024-01-01T00:00:00Z', + 'updated_at' => '2024-01-01T00:00:00Z', + 'closed_at' => null, + ], + ], 200); + + Github::connector()->withMockClient(new MockClient([ + '*' => $openPrsResponse, + ])); + + $allPrs = Github::pullRequests()->paginate('test', 'repo', [ + 'state' => 'open', + 'sort' => 'created', + 'direction' => 'desc', + ]); + + expect($allPrs) + ->toBeArray() + ->toHaveCount(1) + ->and($allPrs[0]->title)->toBe('Open PR'); + }); + + it('throws exception when maximum page limit is exceeded', function () { + $infiniteResponse = MockResponse::make([ + [ + 'id' => 1, + 'number' => 1, + 'state' => 'open', + 'title' => 'PR 1', + 'body' => '', + 'html_url' => 'https://github.com/test/repo/pull/1', + 'diff_url' => 'https://github.com/test/repo/pull/1.diff', + 'patch_url' => 'https://github.com/test/repo/pull/1.patch', + 'base' => ['ref' => 'main'], + 'head' => ['ref' => 'feature-1'], + 'draft' => false, + 'merged' => false, + 'merged_at' => null, + 'merge_commit_sha' => null, + 'comments' => 0, + 'review_comments' => 0, + 'commits' => 1, + 'additions' => 10, + 'deletions' => 5, + 'changed_files' => 2, + 'user' => $this->createMockUserData('testuser', 1), + 'merged_by' => null, + 'created_at' => '2024-01-01T00:00:00Z', + 'updated_at' => '2024-01-01T00:00:00Z', + 'closed_at' => null, + ], + ], 200, [ + 'Link' => '; rel="next"', + ]); + + Github::connector()->withMockClient(new MockClient([ + '*' => $infiniteResponse, + ])); + + expect(fn () => Github::pullRequests()->paginate('test', 'repo')) + ->toThrow(RuntimeException::class, 'Maximum page limit (1000) exceeded during pagination'); + }); +});