Skip to content
137 changes: 131 additions & 6 deletions src/VCS/Adapter/Git/Gitea.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,88 @@ public function createOrganization(string $orgName): string
return $responseBody['name'] ?? '';
}

// Stub methods to satisfy abstract class requirements
// These will be implemented in follow-up PRs

/**
* Search repositories in organization
*
* @param string $installationId Not used in Gitea (kept for interface compatibility)
* @param string $owner Organization or user name
* @param int $page Page number for pagination
* @param int $per_page Number of results per page
* @param string $search Search query to filter repository names
* @return array<mixed> Array with 'items' (repositories) and 'total' count
*/
public function searchRepositories(string $installationId, string $owner, int $page, int $per_page, string $search = ''): array
{
throw new Exception("Not implemented yet");
$filteredRepos = [];
$currentPage = 1;
$maxPages = 50;

$neededForPage = $page * $per_page;
$maxToCollect = $neededForPage + $per_page;

while ($currentPage <= $maxPages) {
$queryParams = [
'page' => $currentPage,
'limit' => 100,
];

if (!empty($search)) {
$queryParams['q'] = $search;
}

$query = http_build_query($queryParams);
$url = "/repos/search?{$query}";

$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
throw new Exception("Repository search failed with status code {$responseHeadersStatusCode}");
}

$responseBody = $response['body'] ?? [];

if (!is_array($responseBody)) {
throw new Exception('Unexpected response body: ' . json_encode($responseBody));
}

if (!array_key_exists('data', $responseBody)) {
throw new Exception("Repositories list missing in response: " . json_encode($responseBody));
}

$repos = $responseBody['data'];

if (empty($repos)) {
break;
}

foreach ($repos as $repo) {
$repoOwner = $repo['owner']['login'] ?? '';
if ($repoOwner === $owner) {
$filteredRepos[] = $repo;

if (count($filteredRepos) >= $maxToCollect) {
break 2;
}
}
}

if (count($repos) < 100) {
break;
}

$currentPage++;
}

$total = count($filteredRepos);
$offset = ($page - 1) * $per_page;
$pagedRepos = array_slice($filteredRepos, $offset, $per_page);

return [
'items' => $pagedRepos,
'total' => $total,
];
}

public function getInstallationRepository(string $repositoryName): array
Expand Down Expand Up @@ -457,7 +533,7 @@ public function getUser(string $username): array

public function getOwnerName(string $installationId): string
{
throw new Exception("Not implemented yet");
throw new Exception("getOwnerName() is not applicable for Gitea");
}

public function getPullRequest(string $owner, string $repositoryName, int $pullRequestNumber): array
Expand Down Expand Up @@ -493,9 +569,58 @@ public function getPullRequestFromBranch(string $owner, string $repositoryName,
return $responseBody[0] ?? [];
}

/**
* List all branches in a repository
*
* @param string $owner Owner of the repository
* @param string $repositoryName Name of the repository
* @return array<string> Array of branch names
*/
public function listBranches(string $owner, string $repositoryName): array
{
throw new Exception("Not implemented yet");
$allBranches = [];
$perPage = 50;
$maxPages = 100;

for ($currentPage = 1; $currentPage <= $maxPages; $currentPage++) {
$url = "/repos/{$owner}/{$repositoryName}/branches?page={$currentPage}&limit={$perPage}";

$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;

if ($responseHeadersStatusCode === 404) {
return [];
}

if ($responseHeadersStatusCode >= 400) {
if ($currentPage === 1) {
throw new Exception("Failed to list branches: HTTP {$responseHeadersStatusCode}");
}
break;
}

$responseBody = $response['body'] ?? [];

if (!is_array($responseBody)) {
break;
}

$pageCount = 0;
foreach ($responseBody as $branch) {
if (is_array($branch) && array_key_exists('name', $branch)) {
$allBranches[] = $branch['name'] ?? '';
$pageCount++;
}
}

if ($pageCount < $perPage) {
break;
}
}

return $allBranches;
}

/**
Expand Down
125 changes: 122 additions & 3 deletions tests/VCS/Adapter/GiteaTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -614,7 +614,84 @@ public function testGetEvent(): void
}
public function testSearchRepositories(): void
{
$this->markTestSkipped('Will be implemented in follow-up PR');
// Create multiple repositories
$repo1Name = 'test-search-repo1-' . \uniqid();
$repo2Name = 'test-search-repo2-' . \uniqid();
$repo3Name = 'other-repo-' . \uniqid();

$this->vcsAdapter->createRepository(self::$owner, $repo1Name, false);
$this->vcsAdapter->createRepository(self::$owner, $repo2Name, false);
$this->vcsAdapter->createRepository(self::$owner, $repo3Name, false);

try {
// Search without filter - should return all
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 10);

$this->assertIsArray($result);
$this->assertArrayHasKey('items', $result);
$this->assertArrayHasKey('total', $result);
$this->assertGreaterThanOrEqual(3, $result['total']);

// Search with filter
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 10, 'test-search');

$this->assertIsArray($result);
$this->assertGreaterThanOrEqual(2, $result['total']);

// Verify the filtered repos are in results
$repoNames = array_column($result['items'], 'name');
$this->assertContains($repo1Name, $repoNames);
$this->assertContains($repo2Name, $repoNames);
} finally {
$this->vcsAdapter->deleteRepository(self::$owner, $repo1Name);
$this->vcsAdapter->deleteRepository(self::$owner, $repo2Name);
$this->vcsAdapter->deleteRepository(self::$owner, $repo3Name);
}
}

public function testSearchRepositoriesPagination(): void
{
$repo1 = 'test-pagination-1-' . \uniqid();
$repo2 = 'test-pagination-2-' . \uniqid();

$this->vcsAdapter->createRepository(self::$owner, $repo1, false);
$this->vcsAdapter->createRepository(self::$owner, $repo2, false);

try {
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 1, 'test-pagination');

$this->assertSame(1, count($result['items']));
$this->assertGreaterThanOrEqual(2, $result['total']);

$result2 = $this->vcsAdapter->searchRepositories('', self::$owner, 2, 1, 'test-pagination');
$this->assertSame(1, count($result2['items']));

$result20 = $this->vcsAdapter->searchRepositories('', self::$owner, 20, 1, 'test-pagination');
$this->assertIsArray($result20);
$this->assertEmpty($result20['items']);

} finally {
$this->vcsAdapter->deleteRepository(self::$owner, $repo1);
$this->vcsAdapter->deleteRepository(self::$owner, $repo2);
}
}

public function testSearchRepositoriesNoResults(): void
{
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 10, 'nonexistent-repo-xyz-' . \uniqid());

$this->assertIsArray($result);
$this->assertEmpty($result['items']);
$this->assertSame(0, $result['total']);
}

public function testSearchRepositoriesInvalidOwner(): void
{
$result = $this->vcsAdapter->searchRepositories('', 'nonexistent-owner-' . \uniqid(), 1, 10);

$this->assertIsArray($result);
$this->assertEmpty($result['items']);
$this->assertSame(0, $result['total']);
}

public function testDeleteRepository(): void
Expand Down Expand Up @@ -645,7 +722,18 @@ public function testDeleteNonExistingRepositoryFails(): void

public function testGetOwnerName(): void
{
$this->markTestSkipped('Will be implemented in follow-up PR');
$this->expectException(\Exception::class);
$this->expectExceptionMessage('not applicable for Gitea');

$this->vcsAdapter->getOwnerName('');
}

public function testGetOwnerNameWithRandomInput(): void
{
$this->expectException(\Exception::class);
$this->expectExceptionMessage('not applicable for Gitea');

$this->vcsAdapter->getOwnerName('random-gibberish-' . \uniqid());
}

public function testGetPullRequestFromBranch(): void
Expand Down Expand Up @@ -760,7 +848,38 @@ public function testCreateFileOnBranch(): void

public function testListBranches(): void
{
$this->markTestSkipped('Will be implemented in follow-up PR');
$repositoryName = 'test-list-branches-' . \uniqid();
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);

try {
// Create initial file on main branch
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');

// Create additional branches
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-1', 'main');
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-2', 'main');

$branches = [];
$maxAttempts = 10;
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
$branches = $this->vcsAdapter->listBranches(self::$owner, $repositoryName);

if (in_array('feature-1', $branches, true) && in_array('feature-2', $branches, true)) {
break;
}

usleep(500000);
}

$this->assertIsArray($branches);
$this->assertNotEmpty($branches);
$this->assertContains('main', $branches);
$this->assertContains('feature-1', $branches);
$this->assertContains('feature-2', $branches);
$this->assertGreaterThanOrEqual(3, count($branches));
} finally {
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
}
}

public function testListRepositoryLanguages(): void
Expand Down
Loading