Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 136 additions & 5 deletions src/VCS/Adapter/Git/GitLab.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,22 +178,138 @@ public function getRepositoryName(string $repositoryId): string

public function getRepositoryTree(string $owner, string $repositoryName, string $branch, bool $recursive = false): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/repository/tree?ref=" . urlencode($branch);

if ($recursive) {
$page = 1;
$allItems = [];
do {
$pagedUrl = $url . "&recursive=true&per_page=100&page={$page}";
$response = $this->call(self::METHOD_GET, $pagedUrl, ['PRIVATE-TOKEN' => $this->accessToken]);
$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
return [];
}
$responseBody = $response['body'] ?? [];
if (!is_array($responseBody) || empty($responseBody)) {
break;
}
$allItems = array_merge($allItems, $responseBody);
$page++;
} while (count($responseBody) === 100);
return array_column($allItems, 'path');
}

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
return [];
}

$responseBody = $response['body'] ?? [];
if (!is_array($responseBody)) {
return [];
}

return array_column($responseBody, 'path');
}

public function getRepositoryContent(string $owner, string $repositoryName, string $path, string $ref = ''): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$encodedPath = urlencode($path);
$url = "/projects/{$projectPath}/repository/files/{$encodedPath}?ref=" . urlencode(empty($ref) ? 'HEAD' : $ref);

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode !== 200) {
throw new \Utopia\VCS\Exception\FileNotFound();
}

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

$content = '';
if (($responseBody['encoding'] ?? '') === 'base64') {
$rawContent = $responseBody['content'] ?? '';
$content = base64_decode($rawContent, true);
if ($content === false) {
throw new \Utopia\VCS\Exception\FileNotFound();
}
} else {
throw new \Utopia\VCS\Exception\FileNotFound();
}

return [
'sha' => $responseBody['blob_id'] ?? '',
'size' => $responseBody['size'] ?? 0,
'content' => $content,
];
}

public function listRepositoryContents(string $owner, string $repositoryName, string $path = '', string $ref = ''): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/repository/tree" . (empty($ref) ? '' : '?ref=' . urlencode($ref));

if (!empty($path)) {
$url .= (empty($ref) ? '?' : '&') . 'path=' . urlencode($path);
}

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
return [];
}

$responseBody = $response['body'] ?? [];
if (!is_array($responseBody)) {
return [];
}

$contents = [];
foreach ($responseBody as $item) {
$type = ($item['type'] ?? '') === 'blob' ? self::CONTENTS_FILE : self::CONTENTS_DIRECTORY;
$contents[] = [
'name' => $item['name'] ?? '',
'size' => 0,
'type' => $type,
];
}

return $contents;
}

public function listRepositoryLanguages(string $owner, string $repositoryName): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/languages";

$response = $this->call(self::METHOD_GET, $url, ['PRIVATE-TOKEN' => $this->accessToken]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
return [];
}

$responseBody = $response['body'] ?? [];
if (!is_array($responseBody)) {
return [];
}

return array_keys($responseBody);
}

public function createFile(string $owner, string $repositoryName, string $filepath, string $content, string $message = 'Add file', string $branch = ''): array
Expand Down Expand Up @@ -225,7 +341,22 @@ public function createFile(string $owner, string $repositoryName, string $filepa

public function createBranch(string $owner, string $repositoryName, string $newBranchName, string $oldBranchName): array
{
throw new Exception("Not implemented");
$ownerPath = $this->getOwnerPath($owner);
$projectPath = urlencode("{$ownerPath}/{$repositoryName}");
$url = "/projects/{$projectPath}/repository/branches";

$response = $this->call(self::METHOD_POST, $url, ['PRIVATE-TOKEN' => $this->accessToken], [
'branch' => $newBranchName,
'ref' => $oldBranchName,
]);

$responseHeaders = $response['headers'] ?? [];
$responseHeadersStatusCode = $responseHeaders['status-code'] ?? 0;
if ($responseHeadersStatusCode >= 400) {
throw new Exception("Failed to create branch {$newBranchName}: HTTP {$responseHeadersStatusCode}");
}

return $response['body'] ?? [];
}

public function createPullRequest(string $owner, string $repositoryName, string $title, string $head, string $base, string $body = ''): array
Expand Down
172 changes: 169 additions & 3 deletions tests/VCS/Adapter/GitLabTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ public function testGetRepository(): void
}
}

public function testListRepositoryContentsNonExistingPath(): void
{
$repositoryName = 'test-list-repository-contents-invalid-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');

try {
$contents = $this->vcsAdapter->listRepositoryContents(static::$owner, $repositoryName, 'non-existing-path');

$this->assertIsArray($contents);
$this->assertEmpty($contents);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testDeleteRepository(): void
{
$repositoryName = 'test-delete-repository-' . \uniqid();
Expand Down Expand Up @@ -386,7 +402,101 @@ public function testGetPullRequestWithInvalidNumber(): void

public function testGetRepositoryTree(): void
{
$this->markTestSkipped('Not implemented for GitLab yet');
$repositoryName = 'test-get-repository-tree-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/main.php', '<?php echo "hello";');
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/lib.php', '<?php // lib');

// Non recursive — root level only
$tree = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, static::$defaultBranch, false);

$this->assertIsArray($tree);
$this->assertContains('README.md', $tree);
$this->assertContains('src', $tree);
$this->assertCount(2, $tree);

// Recursive — all files
$treeRecursive = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, static::$defaultBranch, true);

$this->assertIsArray($treeRecursive);
$this->assertContains('README.md', $treeRecursive);
$this->assertContains('src/main.php', $treeRecursive);
$this->assertContains('src/lib.php', $treeRecursive);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetRepositoryTreeWithInvalidBranch(): void
{
$repositoryName = 'test-get-repository-tree-invalid-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');

try {
$tree = $this->vcsAdapter->getRepositoryTree(static::$owner, $repositoryName, 'non-existing-branch', false);

$this->assertIsArray($tree);
$this->assertEmpty($tree);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetRepositoryContent(): void
{
$repositoryName = 'test-get-repository-content-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$fileContent = '# Hello World';
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', $fileContent);

$result = $this->vcsAdapter->getRepositoryContent(static::$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->assertGreaterThan(0, $result['size']);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetRepositoryContentWithRef(): void
{
$repositoryName = 'test-get-repository-content-ref-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'test.txt', 'main branch content');

$result = $this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'test.txt', static::$defaultBranch);

$this->assertIsArray($result);
$this->assertSame('main branch content', $result['content']);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testGetRepositoryContentFileNotFound(): void
{
$repositoryName = 'test-get-repository-content-not-found-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');

try {
$this->expectException(\Utopia\VCS\Exception\FileNotFound::class);
$this->vcsAdapter->getRepositoryContent(static::$owner, $repositoryName, 'non-existing.txt');
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testListBranches(): void
Expand All @@ -396,11 +506,67 @@ public function testListBranches(): void

public function testListRepositoryLanguages(): void
{
$this->markTestSkipped('Not implemented for GitLab yet');
$repositoryName = 'test-list-repository-languages-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'main.php', '<?php echo "test";');
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'script.js', 'console.log("test");');

sleep(5); // ← increase from 2 to 5

$languages = $this->vcsAdapter->listRepositoryLanguages(static::$owner, $repositoryName);

$this->assertIsArray($languages);
$this->assertNotEmpty($languages);
$this->assertContains('PHP', $languages);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testListRepositoryLanguagesEmptyRepo(): void
{
$repositoryName = 'test-list-repository-languages-empty-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$languages = $this->vcsAdapter->listRepositoryLanguages(static::$owner, $repositoryName);

$this->assertIsArray($languages);
$this->assertEmpty($languages);
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}

public function testListRepositoryContents(): void
{
$this->markTestSkipped('Not implemented for GitLab yet');
$repositoryName = 'test-list-repository-contents-' . \uniqid();
$this->vcsAdapter->createRepository(static::$owner, $repositoryName, false);

try {
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'README.md', '# Test');
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'file1.txt', 'content1');
$this->vcsAdapter->createFile(static::$owner, $repositoryName, 'src/main.php', '<?php');

$contents = $this->vcsAdapter->listRepositoryContents(static::$owner, $repositoryName);

$this->assertIsArray($contents);
$this->assertCount(3, $contents);

$names = array_column($contents, 'name');
$this->assertContains('README.md', $names);
$this->assertContains('file1.txt', $names);
$this->assertContains('src', $names);

foreach ($contents as $item) {
$this->assertArrayHasKey('name', $item);
$this->assertArrayHasKey('type', $item);
$this->assertArrayHasKey('size', $item);
}
} finally {
$this->vcsAdapter->deleteRepository(static::$owner, $repositoryName);
}
}
}