diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 1af30388..608712e0 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -742,19 +742,42 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName, } /** - * Lists branches for a given repository + * Lists branches for a given repository, optionally filtered by a search prefix. * - * @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 List of branch names as array + * When $search is provided, uses GET /repos/{owner}/{repo}/git/matching-refs/heads/{prefix} + * to perform server-side prefix filtering. This endpoint ignores per_page/page params and + * always returns all matching refs in one call; results are then paginated client-side. + * When $search is empty, uses GET /repos/{owner}/{repo}/branches with GitHub's native pagination. + * + * @param string $owner + * @param string $repositoryName + * @param int $perPage Clamped to [1, 100] + * @param int $page Page number (1-based) + * @param string $search Prefix filter; empty returns all branches + * @return array List of branch names */ - 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 $page = 1, string $search = ''): array { - $url = "/repos/$owner/$repositoryName/branches"; $perPage = min(max($perPage, 1), 100); + if ($search !== '') { + $url = "/repos/$owner/$repositoryName/git/matching-refs/heads/" . \str_replace('%2F', '/', \rawurlencode($search)); + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"]); + + $statusCode = $response['headers']['status-code'] ?? 0; + $responseBody = $response['body'] ?? []; + + if ($statusCode < 200 || $statusCode >= 300 || !is_array($responseBody)) { + return []; + } + + $branches = array_map(fn ($ref) => str_replace('refs/heads/', '', $ref['ref'] ?? ''), $responseBody); + $offset = ($page - 1) * $perPage; + + return array_values(array_slice($branches, $offset, $perPage)); + } + + $url = "/repos/$owner/$repositoryName/branches"; $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [ 'page' => $page, 'per_page' => $perPage, @@ -831,15 +854,13 @@ public function getLatestCommit(string $owner, string $repositoryName, string $b $responseBody = $response['body'] ?? []; $responseBodyCommit = $responseBody['commit'] ?? []; $responseBodyCommitAuthor = $responseBodyCommit['author'] ?? []; - $responseBodyAuthor = $responseBody['author'] ?? []; + $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."); } diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index f1c4b2fc..a17ca000 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -548,6 +548,12 @@ public function testListBranchesPagination(): void $all = $adapter->listBranches(static::$owner, $repositoryName, 100, 1); $this->assertEqualsCanonicalizing([static::$defaultBranch, 'branch-a', 'branch-b'], $all); + + $searchResults = $adapter->listBranches(static::$owner, $repositoryName, 100, 1, 'branch'); + $this->assertEqualsCanonicalizing(['branch-a', 'branch-b'], $searchResults); + + $noMatch = $adapter->listBranches(static::$owner, $repositoryName, 100, 1, 'xyz'); + $this->assertEmpty($noMatch); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); }