Skip to content

Commit 9448c32

Browse files
committed
4 endpoints inital implementation
1 parent b986ffb commit 9448c32

2 files changed

Lines changed: 276 additions & 9 deletions

File tree

src/VCS/Adapter/Git/Gitea.php

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,14 @@
66
use Utopia\Cache\Cache;
77
use Utopia\VCS\Adapter\Git;
88
use Utopia\VCS\Exception\RepositoryNotFound;
9+
use Utopia\VCS\Exception\FileNotFound;
910

1011
class Gitea extends Git
1112
{
13+
public const CONTENTS_FILE = 'file';
14+
15+
public const CONTENTS_DIRECTORY = 'dir';
16+
1217
protected string $endpoint = 'http://gitea:3000/api/v1';
1318

1419
protected string $accessToken;
@@ -147,24 +152,94 @@ public function getRepositoryName(string $repositoryId): string
147152

148153
public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array
149154
{
150-
throw new Exception("Not implemented yet");
155+
$url = "/repos/{$owner}/{$repositoryName}/git/trees/{$branch}" . ($recursive ? '?recursive=1' : '');
156+
157+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
158+
159+
if (($response['headers']['status-code'] ?? 0) === 404) {
160+
return [];
161+
}
162+
163+
return array_column($response['body']['tree'] ?? [], 'path');
151164
}
152165

153166
public function listRepositoryLanguages(string $owner, string $repositoryName): array
154167
{
155-
throw new Exception("Not implemented yet");
168+
$url = "/repos/{$owner}/{$repositoryName}/languages";
169+
170+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
171+
172+
if (isset($response['body'])) {
173+
return array_keys($response['body']);
174+
}
175+
176+
return [];
156177
}
157178

158179
public function getRepositoryContent(string $owner, string $repositoryName, string $path, string $ref = ''): array
159180
{
160-
throw new Exception("Not implemented yet");
181+
$url = "/repos/{$owner}/{$repositoryName}/contents/{$path}";
182+
if (!empty($ref)) {
183+
$url .= "?ref={$ref}";
184+
}
185+
186+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
187+
188+
if (($response['headers']['status-code'] ?? 0) !== 200) {
189+
throw new FileNotFound();
190+
}
191+
192+
$encoding = $response['body']['encoding'] ?? '';
193+
$content = '';
194+
195+
if ($encoding === 'base64') {
196+
$content = base64_decode($response['body']['content'] ?? '');
197+
} else {
198+
throw new FileNotFound();
199+
}
200+
201+
return [
202+
'sha' => $response['body']['sha'] ?? '',
203+
'size' => $response['body']['size'] ?? 0,
204+
'content' => $content
205+
];
161206
}
162207

163208
public function listRepositoryContents(string $owner, string $repositoryName, string $path = '', string $ref = ''): array
164209
{
165-
throw new Exception("Not implemented yet");
166-
}
210+
$url = "/repos/{$owner}/{$repositoryName}/contents";
211+
if (!empty($path)) {
212+
$url .= "/{$path}";
213+
}
214+
if (!empty($ref)) {
215+
$url .= "?ref={$ref}";
216+
}
167217

218+
$response = $this->call(self::METHOD_GET, $url, ['Authorization' => "token $this->accessToken"]);
219+
220+
if (($response['headers']['status-code'] ?? 0) === 404) {
221+
return [];
222+
}
223+
224+
$items = [];
225+
if (!empty($response['body'][0])) {
226+
$items = $response['body'];
227+
} elseif (!empty($response['body'])) {
228+
$items = [$response['body']];
229+
}
230+
231+
$contents = [];
232+
foreach ($items as $item) {
233+
$type = $item['type'] ?? 'file';
234+
$contents[] = [
235+
'name' => $item['name'] ?? '',
236+
'size' => $item['size'] ?? 0,
237+
'type' => $type === 'file' ? self::CONTENTS_FILE : self::CONTENTS_DIRECTORY
238+
];
239+
}
240+
241+
return $contents;
242+
}
168243
public function deleteRepository(string $owner, string $repositoryName): bool
169244
{
170245
$url = "/repos/{$owner}/{$repositoryName}";

tests/VCS/Adapter/GiteaTest.php

Lines changed: 196 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,30 @@ private function setupGitea(): void
5656
}
5757
}
5858

59+
/**
60+
* Helper method to create a file in a repository
61+
*/
62+
private function createFile(string $owner, string $repo, string $filepath, string $content, string $message = 'Add file'): void
63+
{
64+
$giteaUrl = System::getEnv('TESTS_GITEA_URL', 'http://gitea:3000') ?? '';
65+
$url = "{$giteaUrl}/api/v1/repos/{$owner}/{$repo}/contents/{$filepath}";
66+
67+
$ch = curl_init($url);
68+
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
69+
curl_setopt($ch, CURLOPT_POST, true);
70+
curl_setopt($ch, CURLOPT_HTTPHEADER, [
71+
'Authorization: token ' . self::$accessToken,
72+
'Content-Type: application/json'
73+
]);
74+
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
75+
'content' => base64_encode($content),
76+
'message' => $message
77+
]));
78+
79+
curl_exec($ch);
80+
curl_close($ch);
81+
}
82+
5983
public function testCreateRepository(): void
6084
{
6185
$owner = self::$owner;
@@ -168,17 +192,159 @@ public function testGetRepositoryWithNonExistingOwner(): void
168192

169193
public function testGetRepositoryTree(): void
170194
{
171-
$this->markTestSkipped('Will be implemented in follow-up PR');
195+
$repositoryName = 'test-get-repository-tree-' . \uniqid();
196+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
197+
198+
// Create files in repo
199+
$this->createFile(self::$owner, $repositoryName, 'README.md', '# Test Repo');
200+
$this->createFile(self::$owner, $repositoryName, 'src/main.php', '<?php echo "hello";');
201+
$this->createFile(self::$owner, $repositoryName, 'src/lib.php', '<?php // library');
202+
203+
// Test non-recursive (should only show root level)
204+
$tree = $this->vcsAdapter->getRepositoryTree(self::$owner, $repositoryName, 'main', false);
205+
206+
$this->assertIsArray($tree);
207+
$this->assertContains('README.md', $tree);
208+
$this->assertContains('src', $tree);
209+
$this->assertCount(2, $tree); // Only README.md and src folder at root
210+
211+
// Test recursive (should show all files including nested)
212+
$treeRecursive = $this->vcsAdapter->getRepositoryTree(self::$owner, $repositoryName, 'main', true);
213+
214+
$this->assertIsArray($treeRecursive);
215+
$this->assertContains('README.md', $treeRecursive);
216+
$this->assertContains('src', $treeRecursive);
217+
$this->assertContains('src/main.php', $treeRecursive);
218+
$this->assertContains('src/lib.php', $treeRecursive);
219+
$this->assertGreaterThanOrEqual(4, count($treeRecursive));
220+
221+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
222+
}
223+
224+
public function testGetRepositoryTreeWithInvalidBranch(): void
225+
{
226+
$repositoryName = 'test-get-repository-tree-invalid-' . \uniqid();
227+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
228+
$this->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
229+
230+
$tree = $this->vcsAdapter->getRepositoryTree(self::$owner, $repositoryName, 'non-existing-branch', false);
231+
232+
$this->assertIsArray($tree);
233+
$this->assertEmpty($tree);
234+
235+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
172236
}
173237

174238
public function testGetRepositoryContent(): void
175239
{
176-
$this->markTestSkipped('Will be implemented in follow-up PR');
240+
$repositoryName = 'test-get-repository-content-' . \uniqid();
241+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
242+
243+
$fileContent = '# Hello World';
244+
$this->createFile(self::$owner, $repositoryName, 'README.md', $fileContent);
245+
246+
$result = $this->vcsAdapter->getRepositoryContent(self::$owner, $repositoryName, 'README.md');
247+
248+
$this->assertIsArray($result);
249+
$this->assertArrayHasKey('content', $result);
250+
$this->assertArrayHasKey('sha', $result);
251+
$this->assertArrayHasKey('size', $result);
252+
$this->assertSame($fileContent, $result['content']);
253+
$this->assertIsString($result['sha']);
254+
$this->assertGreaterThan(0, $result['size']);
255+
256+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
257+
}
258+
259+
public function testGetRepositoryContentWithRef(): void
260+
{
261+
$repositoryName = 'test-get-repository-content-ref-' . \uniqid();
262+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
263+
264+
$this->createFile(self::$owner, $repositoryName, 'test.txt', 'main branch content');
265+
266+
$result = $this->vcsAdapter->getRepositoryContent(self::$owner, $repositoryName, 'test.txt', 'main');
267+
268+
$this->assertIsArray($result);
269+
$this->assertSame('main branch content', $result['content']);
270+
271+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
272+
}
273+
274+
public function testGetRepositoryContentFileNotFound(): void
275+
{
276+
$repositoryName = 'test-get-repository-content-not-found-' . \uniqid();
277+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
278+
$this->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
279+
280+
$this->expectException(\Utopia\VCS\Exception\FileNotFound::class);
281+
$this->vcsAdapter->getRepositoryContent(self::$owner, $repositoryName, 'non-existing.txt');
282+
283+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
177284
}
178285

179286
public function testListRepositoryContents(): void
180287
{
181-
$this->markTestSkipped('Will be implemented in follow-up PR');
288+
$repositoryName = 'test-list-repository-contents-' . \uniqid();
289+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
290+
291+
$this->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
292+
$this->createFile(self::$owner, $repositoryName, 'file1.txt', 'content1');
293+
$this->createFile(self::$owner, $repositoryName, 'src/main.php', '<?php');
294+
295+
// List root directory
296+
$contents = $this->vcsAdapter->listRepositoryContents(self::$owner, $repositoryName);
297+
298+
$this->assertIsArray($contents);
299+
$this->assertCount(3, $contents); // README.md, file1.txt, src folder
300+
301+
$names = array_column($contents, 'name');
302+
$this->assertContains('README.md', $names);
303+
$this->assertContains('file1.txt', $names);
304+
$this->assertContains('src', $names);
305+
306+
// Verify types
307+
foreach ($contents as $item) {
308+
$this->assertArrayHasKey('name', $item);
309+
$this->assertArrayHasKey('type', $item);
310+
$this->assertArrayHasKey('size', $item);
311+
}
312+
313+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
314+
}
315+
316+
public function testListRepositoryContentsInSubdirectory(): void
317+
{
318+
$repositoryName = 'test-list-repository-contents-subdir-' . \uniqid();
319+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
320+
321+
$this->createFile(self::$owner, $repositoryName, 'src/file1.php', '<?php');
322+
$this->createFile(self::$owner, $repositoryName, 'src/file2.php', '<?php');
323+
324+
$contents = $this->vcsAdapter->listRepositoryContents(self::$owner, $repositoryName, 'src');
325+
326+
$this->assertIsArray($contents);
327+
$this->assertCount(2, $contents);
328+
329+
$names = array_column($contents, 'name');
330+
$this->assertContains('file1.php', $names);
331+
$this->assertContains('file2.php', $names);
332+
333+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
334+
}
335+
336+
public function testListRepositoryContentsNonExistingPath(): void
337+
{
338+
$repositoryName = 'test-list-repository-contents-invalid-' . \uniqid();
339+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
340+
$this->createFile(self::$owner, $repositoryName, 'README.md', '# Test');
341+
342+
$contents = $this->vcsAdapter->listRepositoryContents(self::$owner, $repositoryName, 'non-existing-path');
343+
344+
$this->assertIsArray($contents);
345+
$this->assertEmpty($contents);
346+
347+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
182348
}
183349

184350
public function testGetPullRequest(): void
@@ -273,6 +439,32 @@ public function testListBranches(): void
273439

274440
public function testListRepositoryLanguages(): void
275441
{
276-
$this->markTestSkipped('Will be implemented in follow-up PR');
442+
$repositoryName = 'test-list-repository-languages-' . \uniqid();
443+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
444+
445+
$this->createFile(self::$owner, $repositoryName, 'main.php', '<?php echo "test";');
446+
$this->createFile(self::$owner, $repositoryName, 'script.js', 'console.log("test");');
447+
$this->createFile(self::$owner, $repositoryName, 'style.css', 'body { margin: 0; }');
448+
449+
$languages = $this->vcsAdapter->listRepositoryLanguages(self::$owner, $repositoryName);
450+
451+
$this->assertIsArray($languages);
452+
$this->assertNotEmpty($languages);
453+
$this->assertContains('PHP', $languages);
454+
455+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
456+
}
457+
458+
public function testListRepositoryLanguagesEmptyRepo(): void
459+
{
460+
$repositoryName = 'test-list-repository-languages-empty-' . \uniqid();
461+
$this->vcsAdapter->createRepository(self::$owner, $repositoryName, false);
462+
463+
$languages = $this->vcsAdapter->listRepositoryLanguages(self::$owner, $repositoryName);
464+
465+
$this->assertIsArray($languages);
466+
$this->assertEmpty($languages);
467+
468+
$this->vcsAdapter->deleteRepository(self::$owner, $repositoryName);
277469
}
278470
}

0 commit comments

Comments
 (0)