Skip to content
87 changes: 68 additions & 19 deletions src/VCS/Adapter/Git/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -742,32 +742,81 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName,
}

/**
* Lists branches for a given repository
* Lists branches using GitHub GraphQL repository.refs with prefix search and cursor pagination.
*
* @param string $owner Owner name of the repository
* @param string $repositoryName Name of the GitHub repository
* @param int $perPage Number of branches to fetch per page
* @param int $page Page number to start fetching from
* @return array<string> List of branch names as array
* GraphQL refs(query:) does server-side substring filtering — 'ranch' matches 'branch-x'.
* Pass a cursor string from a previous nextCursor as $page to resume pagination; any integer
* value is treated as the first page. perPage is clamped to [1, 100].
*
* We use GraphQL instead of REST because:
* 1. REST GET /repos/{owner}/{repo}/branches has no search/filter parameter.
* 2. REST only supports integer page offsets; GraphQL edges carry cursors for exact resumption.
*
* @param string $owner
* @param string $repositoryName
* @param int $perPage Clamped to [1, 100]
* @param int|string|null $page Pass a cursor string from nextCursor to resume; integers treated as page 1
* @param string $search Prefix filter; empty returns all branches
* @return array{items: array<string>, hasNext: bool, nextCursor: string|null}
*/
public function listBranches(string $owner, string $repositoryName, int $perPage = 100, int $page = 1): array
public function listBranches(string $owner, string $repositoryName, int $perPage = 100, int|string|null $page = 1, string $search = ''): array
Comment thread
greptile-apps[bot] marked this conversation as resolved.
{
$url = "/repos/$owner/$repositoryName/branches";
$perPage = min(max($perPage, 1), 100);

$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [
'page' => $page,
'per_page' => $perPage,
$cursor = is_string($page) ? $page : null;

$gql = <<<'GRAPHQL'
query ListBranches($owner: String!, $name: String!, $first: Int!, $after: String, $query: String) {
repository(owner: $owner, name: $name) {
refs(refPrefix: "refs/heads/", first: $first, after: $after, orderBy: {field: ALPHABETICAL, direction: ASC}, query: $query) {
edges {
cursor
node {
name
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
GRAPHQL;

$response = $this->call(self::METHOD_POST, '/graphql', ['Authorization' => "Bearer $this->accessToken"], [
'query' => $gql,
'variables' => [
'owner' => $owner,
'name' => $repositoryName,
'first' => $perPage,
'after' => $cursor,
'query' => $search !== '' ? $search : null,
],
]);

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

if ($statusCode < 200 || $statusCode >= 300 || !is_array($responseBody)) {
return [];
if ($statusCode < 200 || $statusCode >= 300 || !is_array($responseBody) || array_key_exists('errors', $responseBody)) {
return ['items' => [], 'hasNext' => false, 'nextCursor' => null];
}

$repository = $responseBody['data']['repository'] ?? null;
$refs = is_array($repository) ? ($repository['refs'] ?? null) : null;

if (!is_array($refs)) {
return ['items' => [], 'hasNext' => false, 'nextCursor' => null];
}

return array_values(array_map(fn ($branch) => $branch['name'] ?? '', $responseBody));
$edges = $refs['edges'] ?? [];
$pageInfo = $refs['pageInfo'] ?? [];
$hasNext = (bool) ($pageInfo['hasNextPage'] ?? false);

return [
'items' => array_map(fn ($edge) => $edge['node']['name'] ?? '', $edges),
'hasNext' => $hasNext,
'nextCursor' => $hasNext ? ($pageInfo['endCursor'] ?? null) : null,
];
}

/**
Expand Down Expand Up @@ -831,15 +880,15 @@ public function getLatestCommit(string $owner, string $repositoryName, string $b
$responseBody = $response['body'] ?? [];
$responseBodyCommit = $responseBody['commit'] ?? [];
$responseBodyCommitAuthor = $responseBodyCommit['author'] ?? [];
$responseBodyAuthor = $responseBody['author'] ?? [];
// GitHub sets author to null for commits from App installations whose email
// does not match any GitHub user — treat it as an empty array to allow fallbacks.
$responseBodyAuthor = is_array($responseBody['author'] ?? null) ? $responseBody['author'] : [];

if (
!array_key_exists('name', $responseBodyCommitAuthor) ||
!array_key_exists('message', $responseBodyCommit) ||
!array_key_exists('sha', $responseBody) ||
!array_key_exists('html_url', $responseBody) ||
!array_key_exists('avatar_url', $responseBodyAuthor) ||
!array_key_exists('html_url', $responseBodyAuthor)
!array_key_exists('html_url', $responseBody)
) {
throw new Exception("Latest commit response is missing required information.");
}
Expand Down
Loading
Loading