Skip to content

Commit 43e2053

Browse files
committed
Improve GitHub branch listing pagination
1 parent 2850dbe commit 43e2053

3 files changed

Lines changed: 135 additions & 21 deletions

File tree

src/VCS/Adapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ abstract public function getRepositoryName(string $repositoryId): string;
225225
*
226226
* @param string $owner Owner name of the repository
227227
* @param string $repositoryName Name of the repository
228-
* @return array<string> List of branch names as array
228+
* @return array<string>|array{items: array<string>, hasNext: bool, nextCursor?: string|null} List of branch names or branch names with pagination metadata
229229
*/
230230
abstract public function listBranches(string $owner, string $repositoryName): array;
231231

src/VCS/Adapter/Git/GitHub.php

Lines changed: 86 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -747,27 +747,103 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName,
747747
* @param string $owner Owner name of the repository
748748
* @param string $repositoryName Name of the GitHub repository
749749
* @param int $perPage Number of branches to fetch per page
750-
* @param int $page Page number to start fetching from
751-
* @return array<string> List of branch names as array
750+
* @param int|string|null $page Page number or GraphQL cursor to start fetching from
751+
* @param string $search Branch name search query
752+
* @return array{items: array<string>, hasNext: bool, nextCursor: string|null} List of branch names and pagination metadata
752753
*/
753-
public function listBranches(string $owner, string $repositoryName, int $perPage = 100, int $page = 1): array
754+
public function listBranches(string $owner, string $repositoryName, int $perPage = 100, int|string|null $page = 1, string $search = ''): array
754755
{
755-
$url = "/repos/$owner/$repositoryName/branches";
756756
$perPage = min(max($perPage, 1), 100);
757+
$cursor = is_string($page) ? $page : null;
758+
$page = is_int($page) ? max($page, 1) : 1;
759+
$result = [
760+
'items' => [],
761+
'hasNext' => false,
762+
'nextCursor' => null,
763+
];
764+
765+
for ($currentPage = 1; $currentPage <= $page; $currentPage++) {
766+
$result = $this->listBranchesPage($owner, $repositoryName, $perPage, $cursor, $search);
767+
768+
if ($currentPage === $page) {
769+
return $result;
770+
}
771+
772+
if ($result['hasNext'] === false) {
773+
return [
774+
'items' => [],
775+
'hasNext' => false,
776+
'nextCursor' => null,
777+
];
778+
}
779+
780+
$cursor = $result['nextCursor'];
781+
}
757782

758-
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [
759-
'page' => $page,
760-
'per_page' => $perPage,
783+
return $result;
784+
}
785+
786+
/**
787+
* @return array{items: array<string>, hasNext: bool, nextCursor: string|null}
788+
*/
789+
private function listBranchesPage(string $owner, string $repositoryName, int $perPage, ?string $cursor, string $search): array
790+
{
791+
$query = <<<'GRAPHQL'
792+
query ListBranches($owner: String!, $name: String!, $first: Int!, $after: String, $query: String) {
793+
repository(owner: $owner, name: $name) {
794+
refs(refPrefix: "refs/heads/", first: $first, after: $after, query: $query) {
795+
nodes {
796+
name
797+
}
798+
pageInfo {
799+
hasNextPage
800+
endCursor
801+
}
802+
}
803+
}
804+
}
805+
GRAPHQL;
806+
807+
$response = $this->call(self::METHOD_POST, '/graphql', ['Authorization' => "Bearer $this->accessToken"], [
808+
'query' => $query,
809+
'variables' => [
810+
'owner' => $owner,
811+
'name' => $repositoryName,
812+
'first' => $perPage,
813+
'after' => $cursor,
814+
'query' => $search === '' ? null : $search,
815+
],
761816
]);
762817

763818
$statusCode = $response['headers']['status-code'] ?? 0;
764819
$responseBody = $response['body'] ?? [];
765820

766-
if ($statusCode < 200 || $statusCode >= 300 || !is_array($responseBody)) {
767-
return [];
821+
if ($statusCode < 200 || $statusCode >= 300 || !is_array($responseBody) || array_key_exists('errors', $responseBody)) {
822+
return [
823+
'items' => [],
824+
'hasNext' => false,
825+
'nextCursor' => null,
826+
];
827+
}
828+
829+
$refs = $responseBody['data']['repository']['refs'] ?? null;
830+
831+
if (!is_array($refs)) {
832+
return [
833+
'items' => [],
834+
'hasNext' => false,
835+
'nextCursor' => null,
836+
];
768837
}
769838

770-
return array_values(array_map(fn ($branch) => $branch['name'] ?? '', $responseBody));
839+
$pageInfo = $refs['pageInfo'] ?? [];
840+
$hasNext = $pageInfo['hasNextPage'] ?? false;
841+
842+
return [
843+
'items' => array_values(array_map(fn ($branch) => $branch['name'] ?? '', $refs['nodes'] ?? [])),
844+
'hasNext' => $hasNext,
845+
'nextCursor' => $hasNext ? ($pageInfo['endCursor'] ?? null) : null,
846+
];
771847
}
772848

773849
/**

tests/VCS/Adapter/GitHubTest.php

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,17 @@ public function testListBranches(): void
471471
try {
472472
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
473473

474-
$branches = $this->vcsAdapter->listBranches(static::$owner, $repositoryName);
474+
/** @var GitHub $adapter */
475+
$adapter = $this->vcsAdapter;
476+
$branches = $adapter->listBranches(static::$owner, $repositoryName);
475477

476478
$this->assertIsArray($branches);
477-
$this->assertNotEmpty($branches);
478-
$this->assertContains(static::$defaultBranch, $branches);
479+
$this->assertArrayHasKey('items', $branches);
480+
$this->assertArrayHasKey('hasNext', $branches);
481+
$this->assertNotEmpty($branches['items']);
482+
$this->assertFalse($branches['hasNext']);
483+
$this->assertNull($branches['nextCursor']);
484+
$this->assertContains(static::$defaultBranch, $branches['items']);
479485
} finally {
480486
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
481487
}
@@ -539,13 +545,37 @@ public function testListBranchesPagination(): void
539545
$adapter = $this->vcsAdapter;
540546

541547
$page1 = $adapter->listBranches(static::$owner, $repositoryName, 1, 1);
542-
$this->assertSame(['branch-a'], $page1);
548+
$this->assertSame(['branch-a'], $page1['items']);
549+
$this->assertTrue($page1['hasNext']);
550+
$this->assertNotEmpty($page1['nextCursor']);
543551

544552
$page2 = $adapter->listBranches(static::$owner, $repositoryName, 1, 2);
545-
$this->assertSame(['branch-b'], $page2);
553+
$this->assertSame(['branch-b'], $page2['items']);
554+
$this->assertTrue($page2['hasNext']);
555+
$this->assertNotEmpty($page2['nextCursor']);
556+
557+
$cursorPage2 = $adapter->listBranches(static::$owner, $repositoryName, 1, $page1['nextCursor']);
558+
$this->assertSame($page2, $cursorPage2);
559+
560+
$page3 = $adapter->listBranches(static::$owner, $repositoryName, 1, 3);
561+
$this->assertSame([static::$defaultBranch], $page3['items']);
562+
$this->assertFalse($page3['hasNext']);
563+
$this->assertNull($page3['nextCursor']);
546564

547565
$all = $adapter->listBranches(static::$owner, $repositoryName, 100, 1);
548-
$this->assertEqualsCanonicalizing([static::$defaultBranch, 'branch-a', 'branch-b'], $all);
566+
$this->assertEqualsCanonicalizing([static::$defaultBranch, 'branch-a', 'branch-b'], $all['items']);
567+
$this->assertFalse($all['hasNext']);
568+
$this->assertNull($all['nextCursor']);
569+
570+
$searchPage1 = $adapter->listBranches(static::$owner, $repositoryName, 1, 1, 'branch');
571+
$this->assertSame(['branch-a'], $searchPage1['items']);
572+
$this->assertTrue($searchPage1['hasNext']);
573+
$this->assertNotEmpty($searchPage1['nextCursor']);
574+
575+
$searchPage2 = $adapter->listBranches(static::$owner, $repositoryName, 1, $searchPage1['nextCursor'], 'branch');
576+
$this->assertSame(['branch-b'], $searchPage2['items']);
577+
$this->assertFalse($searchPage2['hasNext']);
578+
$this->assertNull($searchPage2['nextCursor']);
549579
} finally {
550580
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
551581
}
@@ -557,21 +587,29 @@ public function testListBranchesEmptyRepository(): void
557587
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
558588

559589
try {
560-
$branches = $this->vcsAdapter->listBranches(static::$owner, $repositoryName);
590+
/** @var GitHub $adapter */
591+
$adapter = $this->vcsAdapter;
592+
$branches = $adapter->listBranches(static::$owner, $repositoryName);
561593

562594
$this->assertIsArray($branches);
563-
$this->assertEmpty($branches);
595+
$this->assertSame([], $branches['items']);
596+
$this->assertFalse($branches['hasNext']);
597+
$this->assertNull($branches['nextCursor']);
564598
} finally {
565599
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
566600
}
567601
}
568602

569603
public function testListBranchesNonExistingRepository(): void
570604
{
571-
$branches = $this->vcsAdapter->listBranches(static::$owner, 'non-existing-repo-' . \uniqid());
605+
/** @var GitHub $adapter */
606+
$adapter = $this->vcsAdapter;
607+
$branches = $adapter->listBranches(static::$owner, 'non-existing-repo-' . \uniqid());
572608

573609
$this->assertIsArray($branches);
574-
$this->assertEmpty($branches);
610+
$this->assertSame([], $branches['items']);
611+
$this->assertFalse($branches['hasNext']);
612+
$this->assertNull($branches['nextCursor']);
575613
}
576614

577615
public function testGetLatestCommit(): void

0 commit comments

Comments
 (0)