Skip to content

Commit f29fc06

Browse files
authored
Merge pull request #69 from jaysomani/feat/gitea-repository-operations
feat: Add Gitea repository operations endpoints
2 parents 9201e5b + 8d35393 commit f29fc06

File tree

2 files changed

+253
-9
lines changed

2 files changed

+253
-9
lines changed

src/VCS/Adapter/Git/Gitea.php

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,88 @@ public function createOrganization(string $orgName): string
117117
return $responseBody['name'] ?? '';
118118
}
119119

120-
// Stub methods to satisfy abstract class requirements
121-
// These will be implemented in follow-up PRs
122-
120+
/**
121+
* Search repositories in organization
122+
*
123+
* @param string $installationId Not used in Gitea (kept for interface compatibility)
124+
* @param string $owner Organization or user name
125+
* @param int $page Page number for pagination
126+
* @param int $per_page Number of results per page
127+
* @param string $search Search query to filter repository names
128+
* @return array<mixed> Array with 'items' (repositories) and 'total' count
129+
*/
123130
public function searchRepositories(string $installationId, string $owner, int $page, int $per_page, string $search = ''): array
124131
{
125-
throw new Exception("Not implemented yet");
132+
$filteredRepos = [];
133+
$currentPage = 1;
134+
$maxPages = 50;
135+
136+
$neededForPage = $page * $per_page;
137+
$maxToCollect = $neededForPage + $per_page;
138+
139+
while ($currentPage <= $maxPages) {
140+
$queryParams = [
141+
'page' => $currentPage,
142+
'limit' => 100,
143+
];
144+
145+
if (!empty($search)) {
146+
$queryParams['q'] = $search;
147+
}
148+
149+
$query = http_build_query($queryParams);
150+
$url = "/repos/search?{$query}";
151+
152+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
153+
154+
$responseHeaders = $response['headers'] ?? [];
155+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
156+
if ($responseHeadersStatusCode >= 400) {
157+
throw new Exception("Repository search failed with status code {$responseHeadersStatusCode}");
158+
}
159+
160+
$responseBody = $response['body'] ?? [];
161+
162+
if (!is_array($responseBody)) {
163+
throw new Exception('Unexpected response body: ' . json_encode($responseBody));
164+
}
165+
166+
if (!array_key_exists('data', $responseBody)) {
167+
throw new Exception("Repositories list missing in response: " . json_encode($responseBody));
168+
}
169+
170+
$repos = $responseBody['data'];
171+
172+
if (empty($repos)) {
173+
break;
174+
}
175+
176+
foreach ($repos as $repo) {
177+
$repoOwner = $repo['owner']['login'] ?? '';
178+
if ($repoOwner === $owner) {
179+
$filteredRepos[] = $repo;
180+
181+
if (count($filteredRepos) >= $maxToCollect) {
182+
break 2;
183+
}
184+
}
185+
}
186+
187+
if (count($repos) < 100) {
188+
break;
189+
}
190+
191+
$currentPage++;
192+
}
193+
194+
$total = count($filteredRepos);
195+
$offset = ($page - 1) * $per_page;
196+
$pagedRepos = array_slice($filteredRepos, $offset, $per_page);
197+
198+
return [
199+
'items' => $pagedRepos,
200+
'total' => $total,
201+
];
126202
}
127203

128204
public function getInstallationRepository(string $repositoryName): array
@@ -457,7 +533,7 @@ public function getUser(string $username): array
457533

458534
public function getOwnerName(string $installationId): string
459535
{
460-
throw new Exception("Not implemented yet");
536+
throw new Exception("getOwnerName() is not applicable for Gitea");
461537
}
462538

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

572+
/**
573+
* List all branches in a repository
574+
*
575+
* @param string $owner Owner of the repository
576+
* @param string $repositoryName Name of the repository
577+
* @return array<string> Array of branch names
578+
*/
496579
public function listBranches(string $owner, string $repositoryName): array
497580
{
498-
throw new Exception("Not implemented yet");
581+
$allBranches = [];
582+
$perPage = 50;
583+
$maxPages = 100;
584+
585+
for ($currentPage = 1; $currentPage <= $maxPages; $currentPage++) {
586+
$url = "/repos/{$owner}/{$repositoryName}/branches?page={$currentPage}&limit={$perPage}";
587+
588+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
589+
590+
$responseHeaders = $response['headers'] ?? [];
591+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
592+
593+
if ($responseHeadersStatusCode === 404) {
594+
return [];
595+
}
596+
597+
if ($responseHeadersStatusCode >= 400) {
598+
if ($currentPage === 1) {
599+
throw new Exception("Failed to list branches: HTTP {$responseHeadersStatusCode}");
600+
}
601+
break;
602+
}
603+
604+
$responseBody = $response['body'] ?? [];
605+
606+
if (!is_array($responseBody)) {
607+
break;
608+
}
609+
610+
$pageCount = 0;
611+
foreach ($responseBody as $branch) {
612+
if (is_array($branch) && array_key_exists('name', $branch)) {
613+
$allBranches[] = $branch['name'] ?? '';
614+
$pageCount++;
615+
}
616+
}
617+
618+
if ($pageCount < $perPage) {
619+
break;
620+
}
621+
}
622+
623+
return $allBranches;
499624
}
500625

501626
/**

tests/VCS/Adapter/GiteaTest.php

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,84 @@ public function testGetEvent(): void
614614
}
615615
public function testSearchRepositories(): void
616616
{
617-
$this->markTestSkipped('Will be implemented in follow-up PR');
617+
// Create multiple repositories
618+
$repo1Name = 'test-search-repo1-' . \uniqid();
619+
$repo2Name = 'test-search-repo2-' . \uniqid();
620+
$repo3Name = 'other-repo-' . \uniqid();
621+
622+
$this->vcsAdapter->createRepository(self::$owner, $repo1Name, false);
623+
$this->vcsAdapter->createRepository(self::$owner, $repo2Name, false);
624+
$this->vcsAdapter->createRepository(self::$owner, $repo3Name, false);
625+
626+
try {
627+
// Search without filter - should return all
628+
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 10);
629+
630+
$this->assertIsArray($result);
631+
$this->assertArrayHasKey('items', $result);
632+
$this->assertArrayHasKey('total', $result);
633+
$this->assertGreaterThanOrEqual(3, $result['total']);
634+
635+
// Search with filter
636+
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 10, 'test-search');
637+
638+
$this->assertIsArray($result);
639+
$this->assertGreaterThanOrEqual(2, $result['total']);
640+
641+
// Verify the filtered repos are in results
642+
$repoNames = array_column($result['items'], 'name');
643+
$this->assertContains($repo1Name, $repoNames);
644+
$this->assertContains($repo2Name, $repoNames);
645+
} finally {
646+
$this->vcsAdapter->deleteRepository(self::$owner, $repo1Name);
647+
$this->vcsAdapter->deleteRepository(self::$owner, $repo2Name);
648+
$this->vcsAdapter->deleteRepository(self::$owner, $repo3Name);
649+
}
650+
}
651+
652+
public function testSearchRepositoriesPagination(): void
653+
{
654+
$repo1 = 'test-pagination-1-' . \uniqid();
655+
$repo2 = 'test-pagination-2-' . \uniqid();
656+
657+
$this->vcsAdapter->createRepository(self::$owner, $repo1, false);
658+
$this->vcsAdapter->createRepository(self::$owner, $repo2, false);
659+
660+
try {
661+
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 1, 'test-pagination');
662+
663+
$this->assertSame(1, count($result['items']));
664+
$this->assertGreaterThanOrEqual(2, $result['total']);
665+
666+
$result2 = $this->vcsAdapter->searchRepositories('', self::$owner, 2, 1, 'test-pagination');
667+
$this->assertSame(1, count($result2['items']));
668+
669+
$result20 = $this->vcsAdapter->searchRepositories('', self::$owner, 20, 1, 'test-pagination');
670+
$this->assertIsArray($result20);
671+
$this->assertEmpty($result20['items']);
672+
673+
} finally {
674+
$this->vcsAdapter->deleteRepository(self::$owner, $repo1);
675+
$this->vcsAdapter->deleteRepository(self::$owner, $repo2);
676+
}
677+
}
678+
679+
public function testSearchRepositoriesNoResults(): void
680+
{
681+
$result = $this->vcsAdapter->searchRepositories('', self::$owner, 1, 10, 'nonexistent-repo-xyz-' . \uniqid());
682+
683+
$this->assertIsArray($result);
684+
$this->assertEmpty($result['items']);
685+
$this->assertSame(0, $result['total']);
686+
}
687+
688+
public function testSearchRepositoriesInvalidOwner(): void
689+
{
690+
$result = $this->vcsAdapter->searchRepositories('', 'nonexistent-owner-' . \uniqid(), 1, 10);
691+
692+
$this->assertIsArray($result);
693+
$this->assertEmpty($result['items']);
694+
$this->assertSame(0, $result['total']);
618695
}
619696

620697
public function testDeleteRepository(): void
@@ -645,7 +722,18 @@ public function testDeleteNonExistingRepositoryFails(): void
645722

646723
public function testGetOwnerName(): void
647724
{
648-
$this->markTestSkipped('Will be implemented in follow-up PR');
725+
$this->expectException(\Exception::class);
726+
$this->expectExceptionMessage('not applicable for Gitea');
727+
728+
$this->vcsAdapter->getOwnerName('');
729+
}
730+
731+
public function testGetOwnerNameWithRandomInput(): void
732+
{
733+
$this->expectException(\Exception::class);
734+
$this->expectExceptionMessage('not applicable for Gitea');
735+
736+
$this->vcsAdapter->getOwnerName('random-gibberish-' . \uniqid());
649737
}
650738

651739
public function testGetPullRequestFromBranch(): void
@@ -760,7 +848,38 @@ public function testCreateFileOnBranch(): void
760848

761849
public function testListBranches(): void
762850
{
763-
$this->markTestSkipped('Will be implemented in follow-up PR');
851+
$repositoryName = 'test-list-branches-' . \uniqid();
852+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
853+
854+
try {
855+
// Create initial file on main branch
856+
$this->vcsAdapter->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
857+
858+
// Create additional branches
859+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-1', 'main');
860+
$this->vcsAdapter->createBranch(self::$owner, $repositoryName, 'feature-2', 'main');
861+
862+
$branches = [];
863+
$maxAttempts = 10;
864+
for ($attempt = 0; $attempt < $maxAttempts; $attempt++) {
865+
$branches = $this->vcsAdapter->listBranches(self::$owner, $repositoryName);
866+
867+
if (in_array('feature-1', $branches, true) && in_array('feature-2', $branches, true)) {
868+
break;
869+
}
870+
871+
usleep(500000);
872+
}
873+
874+
$this->assertIsArray($branches);
875+
$this->assertNotEmpty($branches);
876+
$this->assertContains('main', $branches);
877+
$this->assertContains('feature-1', $branches);
878+
$this->assertContains('feature-2', $branches);
879+
$this->assertGreaterThanOrEqual(3, count($branches));
880+
} finally {
881+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
882+
}
764883
}
765884

766885
public function testListRepositoryLanguages(): void

0 commit comments

Comments
 (0)