diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index 944b87b..8ec4fe5 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -6,9 +6,14 @@ use Utopia\Cache\Cache; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Exception\RepositoryNotFound; +use Utopia\VCS\Exception\FileNotFound; class Gitea extends Git { + public const CONTENTS_FILE = 'file'; + + public const CONTENTS_DIRECTORY = 'dir'; + protected string $endpoint = 'http://gitea:3000/api/v1'; protected string $accessToken; @@ -147,24 +152,94 @@ public function getRepositoryName(string $repositoryId): string public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array { - throw new Exception("Not implemented yet"); + $url = "/repos/{$owner}/{$repositoryName}/git/trees/{$branch}" . ($recursive ? '?recursive=1' : ''); + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + if (($response['headers']['status-code'] ?? 0) === 404) { + return []; + } + + return array_column($response['body']['tree'] ?? [], 'path'); } public function listRepositoryLanguages(string $owner, string $repositoryName): array { - throw new Exception("Not implemented yet"); + $url = "/repos/{$owner}/{$repositoryName}/languages"; + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + if (isset($response['body'])) { + return array_keys($response['body']); + } + + return []; } public function getRepositoryContent(string $owner, string $repositoryName, string $path, string $ref = ''): array { - throw new Exception("Not implemented yet"); + $url = "/repos/{$owner}/{$repositoryName}/contents/{$path}"; + if (!empty($ref)) { + $url .= "?ref={$ref}"; + } + + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + if (($response['headers']['status-code'] ?? 0) !== 200) { + throw new FileNotFound(); + } + + $encoding = $response['body']['encoding'] ?? ''; + $content = ''; + + if ($encoding === 'base64') { + $content = base64_decode($response['body']['content'] ?? ''); + } else { + throw new FileNotFound(); + } + + return [ + 'sha' => $response['body']['sha'] ?? '', + 'size' => $response['body']['size'] ?? 0, + 'content' => $content + ]; } public function listRepositoryContents(string $owner, string $repositoryName, string $path = '', string $ref = ''): array { - throw new Exception("Not implemented yet"); - } + $url = "/repos/{$owner}/{$repositoryName}/contents"; + if (!empty($path)) { + $url .= "/{$path}"; + } + if (!empty($ref)) { + $url .= "?ref={$ref}"; + } + $response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]); + + if (($response['headers']['status-code'] ?? 0) === 404) { + return []; + } + + $items = []; + if (!empty($response['body'][0])) { + $items = $response['body']; + } elseif (!empty($response['body'])) { + $items = [$response['body']]; + } + + $contents = []; + foreach ($items as $item) { + $type = $item['type'] ?? 'file'; + $contents[] = [ + 'name' => $item['name'] ?? '', + 'size' => $item['size'] ?? 0, + 'type' => $type === 'file' ? self::CONTENTS_FILE : self::CONTENTS_DIRECTORY + ]; + } + + return $contents; + } public function deleteRepository(string $owner, string $repositoryName): bool { $url = "/repos/{$owner}/{$repositoryName}"; diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 236b8cb..dd1f362 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -56,6 +56,30 @@ private function setupGitea(): void } } + /** + * Helper method to create a file in a repository + */ + private function createFile(string $owner, string $repo, string $filepath, string $content, string $message = 'Add file'): void + { + $giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? ''; + $url = "{$giteaUrl}/api/v1/repos/{$owner}/{$repo}/contents/{$filepath}"; + + $ch = curl_init($url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: token ' . self::$accessToken, + 'Content-Type: application/json' + ]); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'content' => base64_encode($content), + 'message' => $message + ])); + + curl_exec($ch); + curl_close($ch); + } + public function testCreateRepository(): void { $owner = self::$owner; @@ -168,17 +192,159 @@ public function testGetRepositoryWithNonExistingOwner(): void public function testGetRepositoryTree(): void { - $this->markTestSkipped('Will be implemented in follow-up PR'); + $repositoryName = 'test-get-repository-tree-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + // Create files in repo + $this->createFile(self::$owner, $repositoryName, 'README.md', '# Test Repo'); + $this->createFile(self::$owner, $repositoryName, 'src/main.php', 'createFile(self::$owner, $repositoryName, 'src/lib.php', 'vcsAdapter->getRepositoryTree(self::$owner, $repositoryName, 'main', false); + + $this->assertIsArray($tree); + $this->assertContains('README.md', $tree); + $this->assertContains('src', $tree); + $this->assertCount(2, $tree); // Only README.md and src folder at root + + // Test recursive (should show all files including nested) + $treeRecursive = $this->vcsAdapter->getRepositoryTree(self::$owner, $repositoryName, 'main', true); + + $this->assertIsArray($treeRecursive); + $this->assertContains('README.md', $treeRecursive); + $this->assertContains('src', $treeRecursive); + $this->assertContains('src/main.php', $treeRecursive); + $this->assertContains('src/lib.php', $treeRecursive); + $this->assertGreaterThanOrEqual(4, count($treeRecursive)); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + + public function testGetRepositoryTreeWithInvalidBranch(): void + { + $repositoryName = 'test-get-repository-tree-invalid-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + $this->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); + + $tree = $this->vcsAdapter->getRepositoryTree(self::$owner, $repositoryName, 'non-existing-branch', false); + + $this->assertIsArray($tree); + $this->assertEmpty($tree); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } public function testGetRepositoryContent(): void { - $this->markTestSkipped('Will be implemented in follow-up PR'); + $repositoryName = 'test-get-repository-content-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + $fileContent = '# Hello World'; + $this->createFile(self::$owner, $repositoryName, 'README.md', $fileContent); + + $result = $this->vcsAdapter->getRepositoryContent(self::$owner, $repositoryName, 'README.md'); + + $this->assertIsArray($result); + $this->assertArrayHasKey('content', $result); + $this->assertArrayHasKey('sha', $result); + $this->assertArrayHasKey('size', $result); + $this->assertSame($fileContent, $result['content']); + $this->assertIsString($result['sha']); + $this->assertGreaterThan(0, $result['size']); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + + public function testGetRepositoryContentWithRef(): void + { + $repositoryName = 'test-get-repository-content-ref-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + $this->createFile(self::$owner, $repositoryName, 'test.txt', 'main branch content'); + + $result = $this->vcsAdapter->getRepositoryContent(self::$owner, $repositoryName, 'test.txt', 'main'); + + $this->assertIsArray($result); + $this->assertSame('main branch content', $result['content']); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + + public function testGetRepositoryContentFileNotFound(): void + { + $repositoryName = 'test-get-repository-content-not-found-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + $this->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); + + $this->expectException(\Utopia\VCS\Exception\FileNotFound::class); + $this->vcsAdapter->getRepositoryContent(self::$owner, $repositoryName, 'non-existing.txt'); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } public function testListRepositoryContents(): void { - $this->markTestSkipped('Will be implemented in follow-up PR'); + $repositoryName = 'test-list-repository-contents-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + $this->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); + $this->createFile(self::$owner, $repositoryName, 'file1.txt', 'content1'); + $this->createFile(self::$owner, $repositoryName, 'src/main.php', 'vcsAdapter->listRepositoryContents(self::$owner, $repositoryName); + + $this->assertIsArray($contents); + $this->assertCount(3, $contents); // README.md, file1.txt, src folder + + $names = array_column($contents, 'name'); + $this->assertContains('README.md', $names); + $this->assertContains('file1.txt', $names); + $this->assertContains('src', $names); + + // Verify types + foreach ($contents as $item) { + $this->assertArrayHasKey('name', $item); + $this->assertArrayHasKey('type', $item); + $this->assertArrayHasKey('size', $item); + } + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + + public function testListRepositoryContentsInSubdirectory(): void + { + $repositoryName = 'test-list-repository-contents-subdir-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + $this->createFile(self::$owner, $repositoryName, 'src/file1.php', 'createFile(self::$owner, $repositoryName, 'src/file2.php', 'vcsAdapter->listRepositoryContents(self::$owner, $repositoryName, 'src'); + + $this->assertIsArray($contents); + $this->assertCount(2, $contents); + + $names = array_column($contents, 'name'); + $this->assertContains('file1.php', $names); + $this->assertContains('file2.php', $names); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + + public function testListRepositoryContentsNonExistingPath(): void + { + $repositoryName = 'test-list-repository-contents-invalid-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + $this->createFile(self::$owner, $repositoryName, 'README.md', '# Test'); + + $contents = $this->vcsAdapter->listRepositoryContents(self::$owner, $repositoryName, 'non-existing-path'); + + $this->assertIsArray($contents); + $this->assertEmpty($contents); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } public function testGetPullRequest(): void @@ -273,6 +439,32 @@ public function testListBranches(): void public function testListRepositoryLanguages(): void { - $this->markTestSkipped('Will be implemented in follow-up PR'); + $repositoryName = 'test-list-repository-languages-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + $this->createFile(self::$owner, $repositoryName, 'main.php', 'createFile(self::$owner, $repositoryName, 'script.js', 'console.log("test");'); + $this->createFile(self::$owner, $repositoryName, 'style.css', 'body { margin: 0; }'); + + $languages = $this->vcsAdapter->listRepositoryLanguages(self::$owner, $repositoryName); + + $this->assertIsArray($languages); + $this->assertNotEmpty($languages); + $this->assertContains('PHP', $languages); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); + } + + public function testListRepositoryLanguagesEmptyRepo(): void + { + $repositoryName = 'test-list-repository-languages-empty-' . \uniqid(); + $this->vcsAdapter->createRepository(self::$owner, $repositoryName, false); + + $languages = $this->vcsAdapter->listRepositoryLanguages(self::$owner, $repositoryName); + + $this->assertIsArray($languages); + $this->assertEmpty($languages); + + $this->vcsAdapter->deleteRepository(self::$owner, $repositoryName); } }