Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
5 changes: 5 additions & 0 deletions src/VCS/Adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,11 @@ public function createCheckRun(
throw new \Exception('createCheckRun() is not implemented for ' . $this->getName());
}

public function getCheckRunByName(string $owner, string $repositoryName, string $ref, string $checkName): int
{
throw new \Exception('getCheckRunByName() is not implemented for ' . $this->getName());
}

/**
* Gets a check run by ID.
*
Expand Down
78 changes: 61 additions & 17 deletions src/VCS/Adapter/Git/GitHub.php
Original file line number Diff line number Diff line change
Expand Up @@ -742,32 +742,58 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName,
}

/**
* Lists branches for a given repository
* Lists branches for a given repository, optionally filtered by a search string.
*
* @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
* @param string $owner
* @param string $repositoryName
* @param int $perPage Clamped to [1, 100]
* @param int $page Page number (1-based)
* @param string $search Substring filter; empty returns all branches
* @return array<string> 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);

$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [
'page' => $page,
'per_page' => $perPage,
$gql = <<<'GRAPHQL'
query ListBranches($owner: String!, $name: String!, $first: Int!, $query: String) {
repository(owner: $owner, name: $name) {
refs(refPrefix: "refs/heads/", first: $first, orderBy: {field: ALPHABETICAL, direction: ASC}, query: $query) {
edges {
node {
name
}
}
}
}
}
GRAPHQL;

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

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

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

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

if (!is_array($refs)) {
return [];
}

return array_values(array_map(fn ($branch) => $branch['name'] ?? '', $responseBody));
return array_map(fn ($edge) => $edge['node']['name'] ?? '', $refs['edges'] ?? []);
}

/**
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -831,15 +857,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.");
}
Expand Down Expand Up @@ -955,6 +979,26 @@ public function createCheckRun(
return $response['body'] ?? [];
}

public function getCheckRunByName(string $owner, string $repositoryName, string $ref, string $checkName): int
{
$url = "/repos/$owner/$repositoryName/commits/$ref/check-runs";

$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "Bearer $this->accessToken"], [
'check_name' => $checkName,
'filter' => 'latest',
'per_page' => 1,
]);

$responseHeadersStatusCode = $response['headers']['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
return 0;
}

$runs = $response['body']['check_runs'] ?? [];

return (int) ($runs[0]['id'] ?? 0);
}

/**
* Gets a check run by ID.
*
Expand Down
166 changes: 166 additions & 0 deletions tests/VCS/Adapter/GitHubTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,13 @@ 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);

// GitHub refs(query:) does substring matching, so 'ranch' matches 'branch-a' and 'branch-b'
$substringSearch = $adapter->listBranches(static::$owner, $repositoryName, 100, 1, 'ranch');
$this->assertEqualsCanonicalizing(['branch-a', 'branch-b'], $substringSearch);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
Expand Down Expand Up @@ -1112,4 +1119,163 @@ public function testUpdateComment(): void
{
$this->markTestSkipped('Requires existing PR — createPullRequest not implemented in GitHub adapter');
}

public function testGetCheckRunByName(): void
{
$repositoryName = 'test-get-check-run-by-name-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
$commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch);
$commitHash = $commit['commitHash'];

$checkRun = $this->vcsAdapter->createCheckRun(
owner: static::$owner,
repositoryName: $repositoryName,
headSha: $commitHash,
name: 'ci/build',
status: 'in_progress',
);

$foundId = $this->vcsAdapter->getCheckRunByName(
static::$owner,
$repositoryName,
$commitHash,
'ci/build'
);

$this->assertEquals($checkRun['id'], $foundId);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetCheckRunByNameNoMatchReturnsZero(): void
{
$repositoryName = 'test-get-check-run-by-name-nomatch-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
$commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch);
$commitHash = $commit['commitHash'];

$this->vcsAdapter->createCheckRun(
owner: static::$owner,
repositoryName: $repositoryName,
headSha: $commitHash,
name: 'ci/build',
status: 'in_progress',
);

$foundId = $this->vcsAdapter->getCheckRunByName(
static::$owner,
$repositoryName,
$commitHash,
'ci/lint'
);

$this->assertEquals(0, $foundId);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetCheckRunByNameInvalidRepositoryReturnsZero(): void
{
$foundId = $this->vcsAdapter->getCheckRunByName(
static::$owner,
'non-existing-repository-' . \uniqid(),
str_repeat('a', 40),
'ci/build'
);

$this->assertEquals(0, $foundId);
}

public function testGetCheckRunByNameReturnsMostRecent(): void
{
$repositoryName = 'test-get-check-run-by-name-recent-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
$commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch);
$commitHash = $commit['commitHash'];

$first = $this->vcsAdapter->createCheckRun(
owner: static::$owner,
repositoryName: $repositoryName,
headSha: $commitHash,
name: 'ci/build',
status: 'in_progress',
);

$second = $this->vcsAdapter->createCheckRun(
owner: static::$owner,
repositoryName: $repositoryName,
headSha: $commitHash,
name: 'ci/build',
status: 'in_progress',
);

$this->assertGreaterThan($first['id'], $second['id']);

$foundId = $this->vcsAdapter->getCheckRunByName(
static::$owner,
$repositoryName,
$commitHash,
'ci/build'
);

$this->assertEquals($second['id'], $foundId);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetCheckRunByNameThenUpdate(): void
{
$repositoryName = 'test-get-check-run-by-name-update-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
$commit = $this->vcsAdapter->getLatestCommit(static::$owner, $repositoryName, static::$defaultBranch);
$commitHash = $commit['commitHash'];

$this->vcsAdapter->createCheckRun(
owner: static::$owner,
repositoryName: $repositoryName,
headSha: $commitHash,
name: 'ci/build',
status: 'in_progress',
);

$checkRunId = $this->vcsAdapter->getCheckRunByName(
static::$owner,
$repositoryName,
$commitHash,
'ci/build'
);

$this->assertGreaterThan(0, $checkRunId);

$updated = $this->vcsAdapter->updateCheckRun(
owner: static::$owner,
repositoryName: $repositoryName,
checkRunId: $checkRunId,
conclusion: 'success',
title: 'Build succeeded.',
summary: 'All steps passed.',
);

$this->assertEquals($checkRunId, $updated['id']);
$this->assertEquals('completed', $updated['status']);
$this->assertEquals('success', $updated['conclusion']);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}
}
Loading