diff --git a/app/controllers/api/vcs.php b/app/controllers/api/vcs.php index 2ffe8687ea..cc5504753e 100644 --- a/app/controllers/api/vcs.php +++ b/app/controllers/api/vcs.php @@ -464,6 +464,67 @@ App::get('/v1/vcs/github/callback') ->redirect($redirectSuccess); }); +App::get('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/contents') + ->desc('Get files and directories of a VCS repository') + ->groups(['api', 'vcs']) + ->label('scope', 'vcs.read') + ->label('sdk.namespace', 'vcs') + ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) + ->label('sdk.method', 'getRepositoryContents') + ->label('sdk.description', '') + ->label('sdk.response.code', Response::STATUS_CODE_OK) + ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) + ->label('sdk.response.model', Response::MODEL_VCS_CONTENT_LIST) + ->param('installationId', '', new Text(256), 'Installation Id') + ->param('providerRepositoryId', '', new Text(256), 'Repository Id') + ->param('providerRootDirectory', '', new Text(256, 0), 'Path to get contents of nested directory', true) + ->inject('gitHub') + ->inject('response') + ->inject('project') + ->inject('dbForConsole') + ->action(function (string $installationId, string $providerRepositoryId, string $providerRootDirectory, GitHub $github, Response $response, Document $project, Database $dbForConsole) { + $installation = $dbForConsole->getDocument('installations', $installationId); + + if ($installation->isEmpty()) { + throw new Exception(Exception::INSTALLATION_NOT_FOUND); + } + + $providerInstallationId = $installation->getAttribute('providerInstallationId'); + $privateKey = System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY'); + $githubAppId = System::getEnv('_APP_VCS_GITHUB_APP_ID'); + $github->initializeVariables($providerInstallationId, $privateKey, $githubAppId); + + $owner = $github->getOwnerName($providerInstallationId); + try { + $repositoryName = $github->getRepositoryName($providerRepositoryId) ?? ''; + if (empty($repositoryName)) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + } catch (RepositoryNotFound $e) { + throw new Exception(Exception::PROVIDER_REPOSITORY_NOT_FOUND); + } + + $contents = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory); + + $vcsContents = []; + foreach ($contents as $content) { + $isDirectory = false; + if($content['type'] === GitHub::CONTENTS_DIRECTORY) { + $isDirectory = true; + } + + $vcsContents[] = new Document([ + 'isDirectory' => $isDirectory, + 'name' => $content['name'] ?? '', + 'size' => $content['size'] ?? 0 + ]); + } + + $response->dynamic(new Document([ + 'contents' => $vcsContents + ]), Response::MODEL_VCS_CONTENT_LIST); + }); + App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:providerRepositoryId/detection') ->desc('Detect runtime settings from source code') ->groups(['api', 'vcs']) @@ -505,6 +566,7 @@ App::post('/v1/vcs/github/installations/:installationId/providerRepositories/:pr } $files = $github->listRepositoryContents($owner, $repositoryName, $providerRootDirectory); + $files = \array_column($files, 'name'); $languages = $github->listRepositoryLanguages($owner, $repositoryName); $detectorFactory = new Detector($files, $languages); @@ -586,6 +648,7 @@ App::get('/v1/vcs/github/installations/:installationId/providerRepositories') return function () use ($repo, $github) { try { $files = $github->listRepositoryContents($repo['organization'], $repo['name'], ''); + $files = \array_column($files, 'name'); $languages = $github->listRepositoryLanguages($repo['organization'], $repo['name']); $detectorFactory = new Detector($files, $languages); diff --git a/composer.json b/composer.json index 192b311822..1423e62841 100644 --- a/composer.json +++ b/composer.json @@ -69,7 +69,7 @@ "utopia-php/storage": "0.18.*", "utopia-php/swoole": "0.8.*", "utopia-php/system": "0.8.*", - "utopia-php/vcs": "0.6.*", + "utopia-php/vcs": "0.7.*", "utopia-php/websocket": "0.1.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", diff --git a/composer.lock b/composer.lock index 5e48445d23..ea17b4e892 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e002600539435ca8eaaace6e73b4004d", + "content-hash": "90c87617f6a2639e3c6c3a1920e7d7de", "packages": [ { "name": "adhocore/jwt", @@ -1569,16 +1569,16 @@ }, { "name": "utopia-php/cache", - "version": "0.10.1", + "version": "0.10.2", "source": { "type": "git", "url": "https://github.com/utopia-php/cache.git", - "reference": "87ee4fc91e50d4ddfef650aa999ea12be3a99583" + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/cache/zipball/87ee4fc91e50d4ddfef650aa999ea12be3a99583", - "reference": "87ee4fc91e50d4ddfef650aa999ea12be3a99583", + "url": "https://api.github.com/repos/utopia-php/cache/zipball/b22c6eb6d308de246b023efd0fc9758aee8b8247", + "reference": "b22c6eb6d308de246b023efd0fc9758aee8b8247", "shasum": "" }, "require": { @@ -1613,9 +1613,9 @@ ], "support": { "issues": "https://github.com/utopia-php/cache/issues", - "source": "https://github.com/utopia-php/cache/tree/0.10.1" + "source": "https://github.com/utopia-php/cache/tree/0.10.2" }, - "time": "2024-06-18T13:20:25+00:00" + "time": "2024-06-25T20:36:35+00:00" }, { "name": "utopia-php/cli", @@ -2756,16 +2756,16 @@ }, { "name": "utopia-php/vcs", - "version": "0.6.7", + "version": "0.7.0", "source": { "type": "git", "url": "https://github.com/utopia-php/vcs.git", - "reference": "8d8ff1ac68e991b95adb6f91fcde8f9bb8f24974" + "reference": "4745fcf385cb8f5b645be447cc1631930853c8af" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/vcs/zipball/8d8ff1ac68e991b95adb6f91fcde8f9bb8f24974", - "reference": "8d8ff1ac68e991b95adb6f91fcde8f9bb8f24974", + "url": "https://api.github.com/repos/utopia-php/vcs/zipball/4745fcf385cb8f5b645be447cc1631930853c8af", + "reference": "4745fcf385cb8f5b645be447cc1631930853c8af", "shasum": "" }, "require": { @@ -2799,9 +2799,9 @@ ], "support": { "issues": "https://github.com/utopia-php/vcs/issues", - "source": "https://github.com/utopia-php/vcs/tree/0.6.7" + "source": "https://github.com/utopia-php/vcs/tree/0.7.0" }, - "time": "2024-06-05T17:38:29+00:00" + "time": "2024-06-26T09:44:52+00:00" }, { "name": "utopia-php/websocket", @@ -2988,16 +2988,16 @@ "packages-dev": [ { "name": "appwrite/sdk-generator", - "version": "0.38.7", + "version": "0.38.8", "source": { "type": "git", "url": "https://github.com/appwrite/sdk-generator.git", - "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a" + "reference": "6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", - "reference": "0a66c1149ef05ed9f45ce1c897c4a0ce9ee5e95a", + "url": "https://api.github.com/repos/appwrite/sdk-generator/zipball/6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef", + "reference": "6367c57ddbcf7b88cacb900c4fe7ef3f28bf38ef", "shasum": "" }, "require": { @@ -3033,9 +3033,9 @@ "description": "Appwrite PHP library for generating API SDKs for multiple programming languages and platforms", "support": { "issues": "https://github.com/appwrite/sdk-generator/issues", - "source": "https://github.com/appwrite/sdk-generator/tree/0.38.7" + "source": "https://github.com/appwrite/sdk-generator/tree/0.38.8" }, - "time": "2024-06-10T00:23:02+00:00" + "time": "2024-06-17T00:42:27+00:00" }, { "name": "doctrine/deprecations", diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index f83ad58756..254b23582c 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -101,6 +101,7 @@ use Appwrite\Utopia\Response\Model\UsageStorage; use Appwrite\Utopia\Response\Model\UsageUsers; use Appwrite\Utopia\Response\Model\User; use Appwrite\Utopia\Response\Model\Variable; +use Appwrite\Utopia\Response\Model\VcsContent; use Appwrite\Utopia\Response\Model\Webhook; use Exception; use Swoole\Http\Response as SwooleHTTPResponse; @@ -234,6 +235,8 @@ class Response extends SwooleResponse public const MODEL_BRANCH = 'branch'; public const MODEL_BRANCH_LIST = 'branchList'; public const MODEL_DETECTION = 'detection'; + public const MODEL_VCS_CONTENT = 'vcsContent'; + public const MODEL_VCS_CONTENT_LIST = 'vcsContentList'; // Functions public const MODEL_FUNCTION = 'function'; @@ -369,6 +372,7 @@ class Response extends SwooleResponse ->setModel(new BaseList('Target list', self::MODEL_TARGET_LIST, 'targets', self::MODEL_TARGET)) ->setModel(new BaseList('Migrations List', self::MODEL_MIGRATION_LIST, 'migrations', self::MODEL_MIGRATION)) ->setModel(new BaseList('Migrations Firebase Projects List', self::MODEL_MIGRATION_FIREBASE_PROJECT_LIST, 'projects', self::MODEL_MIGRATION_FIREBASE_PROJECT)) + ->setModel(new BaseList('VCS Content List', self::MODEL_VCS_CONTENT_LIST, 'contents', self::MODEL_VCS_CONTENT)) // Entities ->setModel(new Database()) ->setModel(new Collection()) @@ -411,6 +415,7 @@ class Response extends SwooleResponse ->setModel(new Installation()) ->setModel(new ProviderRepository()) ->setModel(new Detection()) + ->setModel(new VcsContent()) ->setModel(new Branch()) ->setModel(new Runtime()) ->setModel(new Deployment()) diff --git a/src/Appwrite/Utopia/Response/Model/VcsContent.php b/src/Appwrite/Utopia/Response/Model/VcsContent.php new file mode 100644 index 0000000000..d19f911b0b --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/VcsContent.php @@ -0,0 +1,55 @@ +addRule('size', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Content size in bytes. Only files have size, and for directories, 0 is returned.', + 'default' => 0, + 'required' => false, + 'example' => 1523, + ]) + ->addRule('isDirectory', [ + 'type' => self::TYPE_BOOLEAN, + 'description' => 'If a content is a directory. Directories can be used to check nested contents.', + 'default' => false, + 'required' => false, + 'example' => true + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Name of directory or file.', + 'default' => "", + 'example' => 'Main.java', + 'array' => false, + ]); + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'VcsContents'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_VCS_CONTENT; + } +} diff --git a/tests/e2e/Services/VCS/VCSConsoleClientTest.php b/tests/e2e/Services/VCS/VCSConsoleClientTest.php index f109e250bf..f04667a0f5 100644 --- a/tests/e2e/Services/VCS/VCSConsoleClientTest.php +++ b/tests/e2e/Services/VCS/VCSConsoleClientTest.php @@ -19,9 +19,9 @@ class VCSConsoleClientTest extends Scope use ProjectCustom; use SideConsole; - public string $providerInstallationId = '42954928'; - public string $providerRepositoryId = '705764267'; - public string $providerRepositoryId2 = '708688544'; + public string $providerInstallationId = '42954928'; // appwrite-test + public string $providerRepositoryId = '705764267'; // ruby-starter (public) + public string $providerRepositoryId2 = '708688544'; // function1.4 (private) public function testGitHubAuthorize(): string { @@ -85,6 +85,78 @@ class VCSConsoleClientTest extends Scope $this->assertEquals(404, $runtime['headers']['status-code']); } + /** + * @depends testGitHubAuthorize + */ + public function testContents(string $installationId): void + { + /** + * Test for SUCCESS + */ + + $runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/contents', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $runtime['headers']['status-code']); + $this->assertGreaterThan(0, $runtime['body']['total']); + $this->assertIsArray($runtime['body']['contents']); + $this->assertGreaterThan(0, \count($runtime['body']['contents'])); + + $gemfileContent = null; + foreach ($runtime['body']['contents'] as $content) { + if ($content['name'] === "Gemfile") { + $gemfileContent = $content; + break; + } + } + $this->assertNotNull($gemfileContent); + $this->assertFalse($gemfileContent['isDirectory']); + $this->assertGreaterThan(0, $gemfileContent['size']); // Should be ~50 bytes + $this->assertLessThan(100, $gemfileContent['size']); + + $libContent = null; + foreach ($runtime['body']['contents'] as $content) { + if ($content['name'] === "lib") { + $libContent = $content; + break; + } + } + $this->assertNotNull($libContent); + $this->assertTrue($libContent['isDirectory']); + $this->assertEquals(0, $gemfileContent['size']); + + $runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/' . $this->providerRepositoryId . '/contents?providerRootDirectory=lib', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(200, $runtime['headers']['status-code']); + $this->assertGreaterThan(0, $runtime['body']['total']); + $this->assertIsArray($runtime['body']['contents']); + $this->assertGreaterThan(0, \count($runtime['body']['contents'])); + + $mainRbContent = null; + foreach ($runtime['body']['contents'] as $content) { + if ($content['name'] === "main.rb") { + $mainRbContent = $content; + break; + } + } + $this->assertNotNull($mainRbContent); + $this->assertFalse($mainRbContent['isDirectory']); + $this->assertGreaterThan(0, $gemfileContent['size']); + + /** + * Test for FAILURE + */ + + $runtime = $this->client->call(Client::METHOD_POST, '/vcs/github/installations/' . $installationId . '/providerRepositories/randomRepositoryId/contents', array_merge([ + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders())); + + $this->assertEquals(404, $runtime['headers']['status-code']); + } + /** * @depends testGitHubAuthorize */