Skip to content

Commit 034d2ce

Browse files
committed
feat: add repository content methods for GitLab adapter
1 parent e0dac70 commit 034d2ce

File tree

3 files changed

+288
-8
lines changed

3 files changed

+288
-8
lines changed

docker

Whitespace-only changes.

src/VCS/Adapter/Git/GitLab.php

Lines changed: 119 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -178,22 +178,121 @@ public function getRepositoryName(string $repositoryId): string
178178

179179
public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array
180180
{
181-
throw new Exception("Not implemented");
181+
$ownerPath = $this->getOwnerPath($owner);
182+
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
183+
$url = "/projects/{$projectPath}/repository/tree?ref=" . urlencode($branch);
184+
185+
if ($recursive) {
186+
$url .= "&recursive=true&per_page=100";
187+
}
188+
189+
$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);
190+
191+
$responseHeaders = $response['headers'] ?? [];
192+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
193+
if ($responseHeadersStatusCode === 404) {
194+
return [];
195+
}
196+
197+
if ($responseHeadersStatusCode >= 400) {
198+
return [];
199+
}
200+
201+
$responseBody = $response['body'] ?? [];
202+
if (!is_array($responseBody)) {
203+
return [];
204+
}
205+
206+
return array_column($responseBody, 'path');
182207
}
183208

184209
public function getRepositoryContent(string $owner, string $repositoryName, string $path, string $ref = ''): array
185210
{
186-
throw new Exception("Not implemented");
211+
$ownerPath = $this->getOwnerPath($owner);
212+
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
213+
$encodedPath = urlencode($path);
214+
$url = "/projects/{$projectPath}/repository/files/{$encodedPath}?ref=" . urlencode(empty($ref) ? 'main' : $ref);
215+
216+
$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);
217+
218+
$responseHeaders = $response['headers'] ?? [];
219+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
220+
if ($responseHeadersStatusCode !== 200) {
221+
throw new \Utopia\VCS\Exception\FileNotFound();
222+
}
223+
224+
$responseBody = $response['body'] ?? [];
225+
226+
$content = '';
227+
if (($responseBody['encoding'] ?? '') === 'base64') {
228+
$content = base64_decode($responseBody['content'] ?? '');
229+
} else {
230+
throw new \Utopia\VCS\Exception\FileNotFound();
231+
}
232+
233+
return [
234+
'sha' => $responseBody['blob_id'] ?? '',
235+
'size' => $responseBody['size'] ?? 0,
236+
'content' => $content,
237+
];
187238
}
188239

189240
public function listRepositoryContents(string $owner, string $repositoryName, string $path = '', string $ref = ''): array
190241
{
191-
throw new Exception("Not implemented");
242+
$ownerPath = $this->getOwnerPath($owner);
243+
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
244+
$url = "/projects/{$projectPath}/repository/tree?ref=" . urlencode(empty($ref) ? 'main' : $ref);
245+
246+
if (!empty($path)) {
247+
$url .= "&path=" . urlencode($path);
248+
}
249+
250+
$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);
251+
252+
$responseHeaders = $response['headers'] ?? [];
253+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
254+
if ($responseHeadersStatusCode >= 400) {
255+
return [];
256+
}
257+
258+
$responseBody = $response['body'] ?? [];
259+
if (!is_array($responseBody)) {
260+
return [];
261+
}
262+
263+
$contents = [];
264+
foreach ($responseBody as $item) {
265+
$type = ($item['type'] ?? '') === 'blob' ? self::CONTENTS_FILE : self::CONTENTS_DIRECTORY;
266+
$contents[] = [
267+
'name' => $item['name'] ?? '',
268+
'size' => 0,
269+
'type' => $type,
270+
];
271+
}
272+
273+
return $contents;
192274
}
193275

194276
public function listRepositoryLanguages(string $owner, string $repositoryName): array
195277
{
196-
throw new Exception("Not implemented");
278+
$ownerPath = $this->getOwnerPath($owner);
279+
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
280+
$url = "/projects/{$projectPath}/languages";
281+
282+
$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);
283+
284+
$responseHeaders = $response['headers'] ?? [];
285+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
286+
if ($responseHeadersStatusCode >= 400) {
287+
return [];
288+
}
289+
290+
$responseBody = $response['body'] ?? [];
291+
if (!is_array($responseBody)) {
292+
return [];
293+
}
294+
295+
return array_keys($responseBody);
197296
}
198297

199298
public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array
@@ -225,7 +324,22 @@ public function createFile(string $owner, string $repositoryName, string $filepa
225324

226325
public function createBranch(string $owner, string $repositoryName, string $newBranchName, string $oldBranchName): array
227326
{
228-
throw new Exception("Not implemented");
327+
$ownerPath = $this->getOwnerPath($owner);
328+
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
329+
$url = "/projects/{$projectPath}/repository/branches";
330+
331+
$response = $this->call(self::METHOD_POST, $url, ['PRIVATE-TOKEN' => $this->accessToken], [
332+
'branch' => $newBranchName,
333+
'ref' => $oldBranchName,
334+
]);
335+
336+
$responseHeaders = $response['headers'] ?? [];
337+
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
338+
if ($responseHeadersStatusCode >= 400) {
339+
throw new Exception("Failed to create branch {$newBranchName}: HTTP {$responseHeadersStatusCode}");
340+
}
341+
342+
return $response['body'] ?? [];
229343
}
230344

231345
public function createPullRequest(string $owner, string $repositoryName, string $title, string $head, string $base, string $body = ''): array

tests/VCS/Adapter/GitLabTest.php

Lines changed: 169 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,22 @@ public function testGetRepository(): void
9494
}
9595
}
9696

97+
public function testListRepositoryContentsNonExistingPath(): void
98+
{
99+
$repositoryName = 'test-list-repository-contents-invalid-' . \uniqid();
100+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
101+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
102+
103+
try {
104+
$contents = $this->vcsAdapter->listRepositoryContents(static::$owner, $repositoryName, 'non-existing-path');
105+
106+
$this->assertIsArray($contents);
107+
$this->assertEmpty($contents);
108+
} finally {
109+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
110+
}
111+
}
112+
97113
public function testDeleteRepository(): void
98114
{
99115
$repositoryName = 'test-delete-repository-' . \uniqid();
@@ -386,7 +402,101 @@ public function testGetPullRequestWithInvalidNumber(): void
386402

387403
public function testGetRepositoryTree(): void
388404
{
389-
$this->markTestSkipped('Not implemented for GitLab yet');
405+
$repositoryName = 'test-get-repository-tree-' . \uniqid();
406+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
407+
408+
try {
409+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
410+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/main.php', '<?php echo "hello";');
411+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/lib.php', '<?php // lib');
412+
413+
// Non recursive — root level only
414+
$tree = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, static::$defaultBranch, false);
415+
416+
$this->assertIsArray($tree);
417+
$this->assertContains('README.md', $tree);
418+
$this->assertContains('src', $tree);
419+
$this->assertCount(2, $tree);
420+
421+
// Recursive — all files
422+
$treeRecursive = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, static::$defaultBranch, true);
423+
424+
$this->assertIsArray($treeRecursive);
425+
$this->assertContains('README.md', $treeRecursive);
426+
$this->assertContains('src/main.php', $treeRecursive);
427+
$this->assertContains('src/lib.php', $treeRecursive);
428+
} finally {
429+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
430+
}
431+
}
432+
433+
public function testGetRepositoryTreeWithInvalidBranch(): void
434+
{
435+
$repositoryName = 'test-get-repository-tree-invalid-' . \uniqid();
436+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
437+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
438+
439+
try {
440+
$tree = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, 'non-existing-branch', false);
441+
442+
$this->assertIsArray($tree);
443+
$this->assertEmpty($tree);
444+
} finally {
445+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
446+
}
447+
}
448+
449+
public function testGetRepositoryContent(): void
450+
{
451+
$repositoryName = 'test-get-repository-content-' . \uniqid();
452+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
453+
454+
try {
455+
$fileContent = '# Hello World';
456+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', $fileContent);
457+
458+
$result = $this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'README.md');
459+
460+
$this->assertIsArray($result);
461+
$this->assertArrayHasKey('content', $result);
462+
$this->assertArrayHasKey('sha', $result);
463+
$this->assertArrayHasKey('size', $result);
464+
$this->assertSame($fileContent, $result['content']);
465+
$this->assertGreaterThan(0, $result['size']);
466+
} finally {
467+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
468+
}
469+
}
470+
471+
public function testGetRepositoryContentWithRef(): void
472+
{
473+
$repositoryName = 'test-get-repository-content-ref-' . \uniqid();
474+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
475+
476+
try {
477+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'main branch content');
478+
479+
$result = $this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'test.txt', static::$defaultBranch);
480+
481+
$this->assertIsArray($result);
482+
$this->assertSame('main branch content', $result['content']);
483+
} finally {
484+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
485+
}
486+
}
487+
488+
public function testGetRepositoryContentFileNotFound(): void
489+
{
490+
$repositoryName = 'test-get-repository-content-not-found-' . \uniqid();
491+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
492+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
493+
494+
try {
495+
$this->expectException(\Utopia\VCS\Exception\FileNotFound::class);
496+
$this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'non-existing.txt');
497+
} finally {
498+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
499+
}
390500
}
391501

392502
public function testListBranches(): void
@@ -396,11 +506,67 @@ public function testListBranches(): void
396506

397507
public function testListRepositoryLanguages(): void
398508
{
399-
$this->markTestSkipped('Not implemented for GitLab yet');
509+
$repositoryName = 'test-list-repository-languages-' . \uniqid();
510+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
511+
512+
try {
513+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'main.php', '<?php echo "test";');
514+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'script.js', 'console.log("test");');
515+
516+
sleep(5); // ← increase from 2 to 5
517+
518+
$languages = $this->vcsAdapter->listRepositoryLanguages(static::$owner, $repositoryName);
519+
520+
$this->assertIsArray($languages);
521+
$this->assertNotEmpty($languages);
522+
$this->assertContains('PHP', $languages);
523+
} finally {
524+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
525+
}
526+
}
527+
528+
public function testListRepositoryLanguagesEmptyRepo(): void
529+
{
530+
$repositoryName = 'test-list-repository-languages-empty-' . \uniqid();
531+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
532+
533+
try {
534+
$languages = $this->vcsAdapter->listRepositoryLanguages(static::$owner, $repositoryName);
535+
536+
$this->assertIsArray($languages);
537+
$this->assertEmpty($languages);
538+
} finally {
539+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
540+
}
400541
}
401542

402543
public function testListRepositoryContents(): void
403544
{
404-
$this->markTestSkipped('Not implemented for GitLab yet');
545+
$repositoryName = 'test-list-repository-contents-' . \uniqid();
546+
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
547+
548+
try {
549+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
550+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'file1.txt', 'content1');
551+
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/main.php', '<?php');
552+
553+
$contents = $this->vcsAdapter->listRepositoryContents(static::$owner, $repositoryName);
554+
555+
$this->assertIsArray($contents);
556+
$this->assertCount(3, $contents);
557+
558+
$names = array_column($contents, 'name');
559+
$this->assertContains('README.md', $names);
560+
$this->assertContains('file1.txt', $names);
561+
$this->assertContains('src', $names);
562+
563+
foreach ($contents as $item) {
564+
$this->assertArrayHasKey('name', $item);
565+
$this->assertArrayHasKey('type', $item);
566+
$this->assertArrayHasKey('size', $item);
567+
}
568+
} finally {
569+
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
570+
}
405571
}
406572
}

0 commit comments

Comments
 (0)