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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 73 additions & 19 deletions src/VCS/Adapter/Git/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -742,32 +742,86 @@ 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; prefix semantics are enforced client-side with str_starts_with.
* 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.
Outdated
{
$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];
}

return array_values(array_map(fn ($branch) => $branch['name'] ?? '', $responseBody));
$repository = $responseBody['data']['repository'] ?? null;
$refs = is_array($repository) ? ($repository['refs'] ?? null) : null;

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

$edges = $refs['edges'] ?? [];
$pageInfo = $refs['pageInfo'] ?? [];
$hasNext = (bool) ($pageInfo['hasNextPage'] ?? false);

// GitHub refs(query:) does substring matching; enforce prefix semantics client-side.
if ($search !== '') {
$edges = array_values(array_filter($edges, fn ($edge) => str_starts_with($edge['node']['name'] ?? '', $search)));
}

return [
'items' => array_map(fn ($edge) => $edge['node']['name'] ?? '', $edges),
'hasNext' => $hasNext,
'nextCursor' => $hasNext ? ($pageInfo['endCursor'] ?? null) : null,
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
];
}

/**
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -831,15 +885,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