diff --git a/composer.json b/composer.json index 12a47c75..f36b4b67 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "php": ">=8.0", "adhocore/jwt": "^1.1", "utopia-php/cache": "1.0.*", + "utopia-php/console": "^0.2.0", "utopia-php/fetch": "0.5.*" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 20e2f044..aac27d88 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": "cfe77dc7b88fcdd59e8be1a94f94ca54", + "content-hash": "209e3138b73381dd88b1e663e69877a5", "packages": [ { "name": "adhocore/jwt", @@ -1980,6 +1980,55 @@ }, "time": "2026-03-12T03:39:09+00:00" }, + { + "name": "utopia-php/console", + "version": "0.2.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/console.git", + "reference": "0e580dcae85e80ecf46aefcb910de6415fa72d81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/console/zipball/0e580dcae85e80ecf46aefcb910de6415fa72d81", + "reference": "0e580dcae85e80ecf46aefcb910de6415fa72d81", + "shasum": "" + }, + "require": { + "php": ">=8.0", + "utopia-php/validators": "^0.1.0" + }, + "require-dev": { + "laravel/pint": "1.2.*", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.3", + "squizlabs/php_codesniffer": "^3.6", + "swoole/ide-helper": "4.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Console helpers for logging, prompting, and executing commands", + "keywords": [ + "cli", + "console", + "php", + "terminal", + "utopia" + ], + "support": { + "issues": "https://github.com/utopia-php/console/issues", + "source": "https://github.com/utopia-php/console/tree/0.2.0" + }, + "time": "2026-04-17T13:58:52+00:00" + }, { "name": "utopia-php/fetch", "version": "0.5.1", @@ -2126,6 +2175,51 @@ "source": "https://github.com/utopia-php/telemetry/tree/0.2.0" }, "time": "2025-12-17T07:56:38+00:00" + }, + { + "name": "utopia-php/validators", + "version": "0.1.0", + "source": { + "type": "git", + "url": "https://github.com/utopia-php/validators.git", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/utopia-php/validators/zipball/5c57d5b6cf964f8981807c1d3ea8df620c869080", + "reference": "5c57d5b6cf964f8981807c1d3ea8df620c869080", + "shasum": "" + }, + "require": { + "php": ">=8.0" + }, + "require-dev": { + "laravel/pint": "1.*", + "phpstan/phpstan": "1.*", + "phpunit/phpunit": "11.*" + }, + "type": "library", + "autoload": { + "psr-4": { + "Utopia\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A lightweight collection of reusable validators for Utopia projects", + "keywords": [ + "php", + "utopia", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/utopia-php/validators/issues", + "source": "https://github.com/utopia-php/validators/tree/0.1.0" + }, + "time": "2025-11-18T11:05:46+00:00" } ], "packages-dev": [ diff --git a/src/VCS/Adapter.php b/src/VCS/Adapter.php index 96ae8437..b780df3d 100644 --- a/src/VCS/Adapter.php +++ b/src/VCS/Adapter.php @@ -3,6 +3,7 @@ namespace Utopia\VCS; use Exception; +use Utopia\Command; abstract class Adapter { @@ -191,7 +192,7 @@ abstract public function updateComment(string $owner, string $repositoryName, in /** * Generates a clone command using app access token */ - abstract public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string; + abstract public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): Command; /** * Parses webhook event payload diff --git a/src/VCS/Adapter/Git/GitHub.php b/src/VCS/Adapter/Git/GitHub.php index 56bf657d..49e30aae 100644 --- a/src/VCS/Adapter/Git/GitHub.php +++ b/src/VCS/Adapter/Git/GitHub.php @@ -4,6 +4,7 @@ use Ahc\Jwt\JWT; use Exception; +use Utopia\Command; use Utopia\Cache\Cache; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Exception\FileNotFound; @@ -841,7 +842,7 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s /** * Generates a clone command using app access token */ - public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string + public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): Command { if (empty($rootDirectory)) { $rootDirectory = '*'; @@ -854,42 +855,133 @@ public function generateCloneCommand(string $owner, string $repositoryName, stri $cloneUrl = "https://{$owner}{$accessToken}@github.com/{$owner}/{$repositoryName}"; - $directory = escapeshellarg($directory); - $rootDirectory = escapeshellarg($rootDirectory); - $commands = [ - "mkdir -p {$directory}", - "cd {$directory}", - "git config --global init.defaultBranch main", - "git init", - "git remote add origin {$cloneUrl}", - // Enable sparse checkout - "git config core.sparseCheckout true", - "echo {$rootDirectory} >> .git/info/sparse-checkout", - // Disable fetching of refs we don't need - "git config --add remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'", - // Disable fetching of tags - "git config remote.origin.tagopt --no-tags", + (new Command('mkdir')) + ->flag('-p') + ->argument($directory), + (new Command('git')) + ->argument('config') + ->argument('--global') + ->argument('init.defaultBranch') + ->argument('main'), + (new Command('git')) + ->argument('init') + ->argument($directory), + (new Command('git')) + ->option('-C', $directory) + ->argument('remote') + ->argument('add') + ->argument('origin') + ->argument($cloneUrl), + (new Command('git')) + ->option('-C', $directory) + ->argument('config') + ->argument('--add') + ->argument('remote.origin.fetch') + ->argument('+refs/heads/*:refs/remotes/origin/*'), + (new Command('git')) + ->option('-C', $directory) + ->argument('config') + ->argument('remote.origin.tagopt') + ->argument('--no-tags'), + (new Command('git')) + ->option('-C', $directory) + ->argument('sparse-checkout') + ->argument('set') + ->argument('--no-cone') + ->argument($rootDirectory), ]; switch ($versionType) { case self::CLONE_TYPE_BRANCH: - $branchName = escapeshellarg($version); - $commands[] = "if git ls-remote --exit-code --heads origin {$branchName}; then git pull --depth=1 origin {$branchName} && git checkout {$branchName}; else git checkout -b {$branchName}; fi"; + $commands[] = Command::or( + Command::and( + (new Command('git')) + ->option('-C', $directory) + ->argument('ls-remote') + ->argument('--exit-code') + ->argument('--heads') + ->argument('origin') + ->argument($version), + (new Command('git')) + ->option('-C', $directory) + ->argument('pull') + ->argument('--depth=1') + ->argument('origin') + ->argument($version), + (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument($version) + ), + (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument('-b') + ->argument($version) + ); break; case self::CLONE_TYPE_COMMIT: - $commitHash = escapeshellarg($version); - $commands[] = "git fetch --depth=1 origin {$commitHash} && git checkout {$commitHash}"; + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('fetch') + ->argument('--depth=1') + ->argument('origin') + ->argument($version); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument($version); break; case self::CLONE_TYPE_TAG: - $tagName = escapeshellarg($version); - $commands[] = "git fetch --depth=1 origin refs/tags/$(git ls-remote --tags origin {$tagName} | tail -n 1 | awk -F '/' '{print $3}') && git checkout FETCH_HEAD"; + $resolvedTag = $this->resolveTagReference($owner, $repositoryName, $version); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('fetch') + ->argument('--depth=1') + ->argument('origin') + ->argument('refs/tags/' . $resolvedTag); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument('FETCH_HEAD'); break; } - $fullCommand = implode(" && ", $commands); + return Command::and(...$commands); + } + + private function resolveTagReference(string $owner, string $repositoryName, string $version): string + { + if (!str_contains($version, '*')) { + return $version; + } + + $prefix = rtrim(strstr($version, '*', true) ?: '', '.'); + $refPrefix = 'tags' . (!empty($prefix) ? '/' . $prefix : ''); + $response = $this->call( + self::METHOD_GET, + "/repos/{$owner}/{$repositoryName}/git/matching-refs/{$refPrefix}", + ['Authorization' => "Bearer $this->accessToken"] + ); + + $refs = $response['body'] ?? []; + $matches = []; + + foreach ($refs as $ref) { + $tag = str_replace('refs/tags/', '', $ref['ref'] ?? ''); + if ($tag !== '' && fnmatch($version, $tag)) { + $matches[] = $tag; + } + } + + if (empty($matches)) { + throw new Exception("Tag not found for pattern: {$version}"); + } + + usort($matches, static fn (string $left, string $right): int => version_compare($left, $right)); - return $fullCommand; + return $matches[array_key_last($matches)]; } /** diff --git a/src/VCS/Adapter/Git/GitLab.php b/src/VCS/Adapter/Git/GitLab.php index 15eeb98c..2302e0ed 100644 --- a/src/VCS/Adapter/Git/GitLab.php +++ b/src/VCS/Adapter/Git/GitLab.php @@ -3,6 +3,7 @@ namespace Utopia\VCS\Adapter\Git; use Exception; +use Utopia\Command; use Utopia\Cache\Cache; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Exception\RepositoryNotFound; @@ -345,7 +346,7 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s throw new Exception("Not implemented"); } - public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string + public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): Command { if (empty($rootDirectory) || $rootDirectory === '/') { $rootDirectory = '*'; @@ -359,40 +360,101 @@ public function generateCloneCommand(string $owner, string $repositoryName, stri $baseUrl = str_replace('://', '://oauth2:' . urlencode($this->accessToken) . '@', $this->gitlabUrl); } - $cloneUrl = escapeshellarg("{$baseUrl}/{$ownerPath}/{$repositoryName}.git"); - $directory = escapeshellarg($directory); - $rootDirectory = escapeshellarg($rootDirectory); - $commands = [ - "mkdir -p {$directory}", - "cd {$directory}", - "git config --global init.defaultBranch main", - "git init", - "git remote add origin {$cloneUrl}", - "git config core.sparseCheckout true", - "echo {$rootDirectory} >> .git/info/sparse-checkout", - "git config --add remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'", - "git config remote.origin.tagopt --no-tags", + (new Command('mkdir')) + ->flag('-p') + ->argument($directory), + (new Command('git')) + ->argument('config') + ->argument('--global') + ->argument('init.defaultBranch') + ->argument('main'), + (new Command('git')) + ->argument('init') + ->argument($directory), + (new Command('git')) + ->option('-C', $directory) + ->argument('remote') + ->argument('add') + ->argument('origin') + ->argument("{$baseUrl}/{$ownerPath}/{$repositoryName}.git"), + (new Command('git')) + ->option('-C', $directory) + ->argument('config') + ->argument('--add') + ->argument('remote.origin.fetch') + ->argument('+refs/heads/*:refs/remotes/origin/*'), + (new Command('git')) + ->option('-C', $directory) + ->argument('config') + ->argument('remote.origin.tagopt') + ->argument('--no-tags'), + (new Command('git')) + ->option('-C', $directory) + ->argument('sparse-checkout') + ->argument('set') + ->argument('--no-cone') + ->argument($rootDirectory), ]; switch ($versionType) { case self::CLONE_TYPE_BRANCH: - $branchName = escapeshellarg($version); - $commands[] = "if git ls-remote --exit-code --heads origin {$branchName}; then git pull --depth=1 origin {$branchName} && git checkout {$branchName}; else git checkout -b {$branchName}; fi"; + $commands[] = Command::or( + Command::and( + (new Command('git')) + ->option('-C', $directory) + ->argument('ls-remote') + ->argument('--exit-code') + ->argument('--heads') + ->argument('origin') + ->argument($version), + (new Command('git')) + ->option('-C', $directory) + ->argument('pull') + ->argument('--depth=1') + ->argument('origin') + ->argument($version), + (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument($version) + ), + (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument('-b') + ->argument($version) + ); break; case self::CLONE_TYPE_COMMIT: - $commitHash = escapeshellarg($version); - $commands[] = "git fetch --depth=1 origin {$commitHash} && git checkout {$commitHash}"; + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('fetch') + ->argument('--depth=1') + ->argument('origin') + ->argument($version); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument($version); break; case self::CLONE_TYPE_TAG: - $tagName = escapeshellarg($version); - $commands[] = "git fetch --depth=1 origin refs/tags/{$tagName} && git checkout FETCH_HEAD"; + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('fetch') + ->argument('--depth=1') + ->argument('origin') + ->argument('refs/tags/' . $version); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument('FETCH_HEAD'); break; default: throw new Exception("Unsupported clone type: {$versionType}"); } - return implode(' && ', $commands); + return Command::and(...$commands); } public function getEvent(string $event, string $payload): array diff --git a/src/VCS/Adapter/Git/Gitea.php b/src/VCS/Adapter/Git/Gitea.php index bc6544ef..4310e2a6 100644 --- a/src/VCS/Adapter/Git/Gitea.php +++ b/src/VCS/Adapter/Git/Gitea.php @@ -3,6 +3,7 @@ namespace Utopia\VCS\Adapter\Git; use Exception; +use Utopia\Command; use Utopia\Cache\Cache; use Utopia\VCS\Adapter\Git; use Utopia\VCS\Exception\RepositoryNotFound; @@ -892,50 +893,114 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s * @param string $versionType Type: branch, commit, or tag * @param string $directory Directory to clone into * @param string $rootDirectory Root directory for sparse checkout - * @return string Shell command to execute + * @return Command Clone command to execute */ - public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string + public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): Command { + if (empty($rootDirectory) || $rootDirectory === '/') { + $rootDirectory = '*'; + } + $cloneUrl = "{$this->giteaUrl}/{$owner}/{$repositoryName}"; if (!empty($this->accessToken)) { $cloneUrl = str_replace('://', "://{$owner}:{$this->accessToken}@", $this->giteaUrl) . "/{$owner}/{$repositoryName}"; } - // SECURITY FIX: Escape clone URL - $cloneUrl = escapeshellarg($cloneUrl); - $directory = escapeshellarg($directory); - $rootDirectory = escapeshellarg($rootDirectory); - $commands = [ - "mkdir -p {$directory}", - "cd {$directory}", - "git config --global init.defaultBranch main", - "git init", - "git remote add origin {$cloneUrl}", - "git config core.sparseCheckout true", - "echo {$rootDirectory} >> .git/info/sparse-checkout", - "git config --add remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'", - "git config remote.origin.tagopt --no-tags", + (new Command('mkdir')) + ->flag('-p') + ->argument($directory), + (new Command('git')) + ->argument('config') + ->argument('--global') + ->argument('init.defaultBranch') + ->argument('main'), + (new Command('git')) + ->argument('init') + ->argument($directory), + (new Command('git')) + ->option('-C', $directory) + ->argument('remote') + ->argument('add') + ->argument('origin') + ->argument($cloneUrl), + (new Command('git')) + ->option('-C', $directory) + ->argument('config') + ->argument('--add') + ->argument('remote.origin.fetch') + ->argument('+refs/heads/*:refs/remotes/origin/*'), + (new Command('git')) + ->option('-C', $directory) + ->argument('config') + ->argument('remote.origin.tagopt') + ->argument('--no-tags'), + (new Command('git')) + ->option('-C', $directory) + ->argument('sparse-checkout') + ->argument('set') + ->argument('--no-cone') + ->argument($rootDirectory), ]; switch ($versionType) { case self::CLONE_TYPE_BRANCH: - $branchName = escapeshellarg($version); - $commands[] = "if git ls-remote --exit-code --heads origin {$branchName}; then git pull --depth=1 origin {$branchName} && git checkout {$branchName}; else git checkout -b {$branchName}; fi"; + $commands[] = Command::or( + Command::and( + (new Command('git')) + ->option('-C', $directory) + ->argument('ls-remote') + ->argument('--exit-code') + ->argument('--heads') + ->argument('origin') + ->argument($version), + (new Command('git')) + ->option('-C', $directory) + ->argument('pull') + ->argument('--depth=1') + ->argument('origin') + ->argument($version), + (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument($version) + ), + (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument('-b') + ->argument($version) + ); break; case self::CLONE_TYPE_COMMIT: - $commitHash = escapeshellarg($version); - $commands[] = "git fetch --depth=1 origin {$commitHash} && git checkout {$commitHash}"; + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('fetch') + ->argument('--depth=1') + ->argument('origin') + ->argument($version); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument($version); break; case self::CLONE_TYPE_TAG: - $tagName = escapeshellarg($version); - $commands[] = "git fetch --depth=1 origin refs/tags/{$version} && git checkout FETCH_HEAD"; + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('fetch') + ->argument('--depth=1') + ->argument('origin') + ->argument('refs/tags/' . $version); + $commands[] = (new Command('git')) + ->option('-C', $directory) + ->argument('checkout') + ->argument('FETCH_HEAD'); break; default: throw new Exception("Unsupported clone type: {$versionType}"); } - return implode(' && ', $commands); + return Command::and(...$commands); } /** diff --git a/tests/VCS/Adapter/GitHubTest.php b/tests/VCS/Adapter/GitHubTest.php index eec05722..debe284d 100644 --- a/tests/VCS/Adapter/GitHubTest.php +++ b/tests/VCS/Adapter/GitHubTest.php @@ -2,8 +2,11 @@ namespace Utopia\Tests\Adapter; +use Exception; +use Utopia\Command; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; +use Utopia\Console; use Utopia\System\System; use Utopia\Tests\Base; use Utopia\VCS\Adapter\Git; @@ -374,12 +377,12 @@ public function testGenerateCloneCommand(): void { \exec('rm -rf /tmp/clone-branch'); $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', 'test', GitHub::CLONE_TYPE_BRANCH, '/tmp/clone-branch', '*'); - $this->assertNotEmpty($gitCloneCommand); - $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); + $this->assertInstanceOf(Command::class, $gitCloneCommand); + $this->assertStringContainsString('sparse-checkout', $gitCloneCommand->toString()); $output = ''; - $resultCode = null; - \exec($gitCloneCommand, $output, $resultCode); + $stderr = ''; + $resultCode = Console::execute($gitCloneCommand, '', $output, $stderr, 30); $this->assertSame(0, $resultCode); $this->assertFileExists('/tmp/clone-branch/README.md'); @@ -389,12 +392,12 @@ public function testGenerateCloneCommandWithCommitHash(): void { \exec('rm -rf /tmp/clone-commit'); $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', '4fb10447faea8a55c5cad7b5ebdfdbedca349fe4', GitHub::CLONE_TYPE_COMMIT, '/tmp/clone-commit', '*'); - $this->assertNotEmpty($gitCloneCommand); - $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); + $this->assertInstanceOf(Command::class, $gitCloneCommand); + $this->assertStringContainsString('sparse-checkout', $gitCloneCommand->toString()); $output = ''; - $resultCode = null; - \exec($gitCloneCommand, $output, $resultCode); + $stderr = ''; + $resultCode = Console::execute($gitCloneCommand, '', $output, $stderr, 30); $this->assertSame(0, $resultCode); $this->assertFileExists('/tmp/clone-commit/README.md'); @@ -404,48 +407,40 @@ public function testGenerateCloneCommandWithTag(): void { \exec('rm -rf /tmp/clone-tag /tmp/clone-tag2 /tmp/clone-tag3'); $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', '0.1.0', GitHub::CLONE_TYPE_TAG, '/tmp/clone-tag', '*'); - $this->assertNotEmpty($gitCloneCommand); - $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); + $this->assertInstanceOf(Command::class, $gitCloneCommand); + $this->assertStringContainsString('sparse-checkout', $gitCloneCommand->toString()); $output = ''; - $resultCode = null; - \exec($gitCloneCommand, $output, $resultCode); + $stderr = ''; + $resultCode = Console::execute($gitCloneCommand, '', $output, $stderr, 30); $this->assertSame(0, $resultCode); $this->assertFileExists('/tmp/clone-tag/README.md'); $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', '0.1.*', GitHub::CLONE_TYPE_TAG, '/tmp/clone-tag2', '*'); - $this->assertNotEmpty($gitCloneCommand); - $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); + $this->assertInstanceOf(Command::class, $gitCloneCommand); + $this->assertStringContainsString('sparse-checkout', $gitCloneCommand->toString()); $output = ''; - $resultCode = null; - \exec($gitCloneCommand, $output, $resultCode); + $stderr = ''; + $resultCode = Console::execute($gitCloneCommand, '', $output, $stderr, 30); $this->assertSame(0, $resultCode); $this->assertFileExists('/tmp/clone-tag2/README.md'); $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', '0.*.*', GitHub::CLONE_TYPE_TAG, '/tmp/clone-tag3', '*'); - $this->assertNotEmpty($gitCloneCommand); - $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); + $this->assertInstanceOf(Command::class, $gitCloneCommand); + $this->assertStringContainsString('sparse-checkout', $gitCloneCommand->toString()); $output = ''; - $resultCode = null; - \exec($gitCloneCommand, $output, $resultCode); + $stderr = ''; + $resultCode = Console::execute($gitCloneCommand, '', $output, $stderr, 30); $this->assertSame(0, $resultCode); $this->assertFileExists('/tmp/clone-tag3/README.md'); - - $gitCloneCommand = $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', '0.2.*', GitHub::CLONE_TYPE_TAG, '/tmp/clone-tag4', '*'); - $this->assertNotEmpty($gitCloneCommand); - $this->assertStringContainsString('sparse-checkout', $gitCloneCommand); - - $output = ''; - $resultCode = null; - \exec($gitCloneCommand, $output, $resultCode); - $this->assertNotEquals(0, $resultCode); - + $this->expectException(Exception::class); + $this->vcsAdapter->generateCloneCommand('test-kh', 'test2', '0.2.*', GitHub::CLONE_TYPE_TAG, '/tmp/clone-tag4', '*'); $this->assertFileDoesNotExist('/tmp/clone-tag4/README.md'); } diff --git a/tests/VCS/Adapter/GitLabTest.php b/tests/VCS/Adapter/GitLabTest.php index 335d44e7..bdb942d5 100644 --- a/tests/VCS/Adapter/GitLabTest.php +++ b/tests/VCS/Adapter/GitLabTest.php @@ -2,8 +2,10 @@ namespace Utopia\Tests\Adapter; +use Utopia\Command; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; +use Utopia\Console; use Utopia\System\System; use Utopia\Tests\Base; use Utopia\VCS\Adapter\Git; @@ -155,15 +157,17 @@ public function testGenerateCloneCommand(): void '/' ); - $this->assertIsString($command); - $this->assertStringContainsString('git init', $command); - $this->assertStringContainsString('git remote add origin', $command); - $this->assertStringContainsString('git config core.sparseCheckout true', $command); - $this->assertStringContainsString($repositoryName, $command); - - $output = []; - \exec($command . ' 2>&1', $output, $exitCode); - $this->assertSame(0, $exitCode, implode("\n", $output)); + $this->assertInstanceOf(Command::class, $command); + $commandString = $command->toString(); + $this->assertStringContainsString('git', $commandString); + $this->assertStringContainsString('remote', $commandString); + $this->assertStringContainsString('sparse-checkout', $commandString); + $this->assertStringContainsString($repositoryName, $commandString); + + $output = ''; + $stderr = ''; + $exitCode = Console::execute($command, '', $output, $stderr, 30); + $this->assertSame(0, $exitCode, $stdout = trim($output . "\n" . $stderr)); $this->assertFileExists($directory . '/README.md'); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); @@ -194,9 +198,11 @@ public function testGenerateCloneCommandWithCommitHash(): void '/' ); - $this->assertIsString($command); - $this->assertStringContainsString('git fetch --depth=1', $command); - $this->assertStringContainsString($commitHash, $command); + $this->assertInstanceOf(Command::class, $command); + $commandString = $command->toString(); + $this->assertStringContainsString('fetch', $commandString); + $this->assertStringContainsString('--depth=1', $commandString); + $this->assertStringContainsString($commitHash, $commandString); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } @@ -225,9 +231,10 @@ public function testGenerateCloneCommandWithTag(): void '/' ); - $this->assertIsString($command); - $this->assertStringContainsString('refs/tags', $command); - $this->assertStringContainsString('v1.0.0', $command); + $this->assertInstanceOf(Command::class, $command); + $commandString = $command->toString(); + $this->assertStringContainsString('refs/tags', $commandString); + $this->assertStringContainsString('v1.0.0', $commandString); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } @@ -247,8 +254,11 @@ public function testGenerateCloneCommandWithInvalidRepository(): void '/' ); - $output = []; - \exec($command . ' 2>&1', $output, $exitCode); + $this->assertInstanceOf(Command::class, $command); + + $output = ''; + $stderr = ''; + $exitCode = Console::execute($command, '', $output, $stderr, 30); $cloneFailed = ($exitCode !== 0) || !file_exists($directory . '/README.md'); $this->assertTrue($cloneFailed, 'Clone should have failed for nonexistent repository'); diff --git a/tests/VCS/Adapter/GiteaTest.php b/tests/VCS/Adapter/GiteaTest.php index 861b9a9b..752d337e 100644 --- a/tests/VCS/Adapter/GiteaTest.php +++ b/tests/VCS/Adapter/GiteaTest.php @@ -2,8 +2,10 @@ namespace Utopia\Tests\Adapter; +use Utopia\Command; use Utopia\Cache\Adapter\None; use Utopia\Cache\Cache; +use Utopia\Console; use Utopia\System\System; use Utopia\Tests\Base; use Utopia\VCS\Adapter\Git; @@ -547,11 +549,12 @@ public function testGenerateCloneCommand(): void '/' ); - $this->assertIsString($command); - $this->assertStringContainsString('git init', $command); - $this->assertStringContainsString('git remote add origin', $command); - $this->assertStringContainsString('git config core.sparseCheckout true', $command); - $this->assertStringContainsString($repositoryName, $command); + $this->assertInstanceOf(Command::class, $command); + $commandString = $command->toString(); + $this->assertStringContainsString('git', $commandString); + $this->assertStringContainsString('remote', $commandString); + $this->assertStringContainsString('sparse-checkout', $commandString); + $this->assertStringContainsString($repositoryName, $commandString); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } @@ -576,9 +579,11 @@ public function testGenerateCloneCommandWithCommitHash(): void '/tmp/test-clone-commit-' . \uniqid(), '/' ); - $this->assertIsString($command); - $this->assertStringContainsString('git fetch --depth=1', $command); - $this->assertStringContainsString($commitHash, $command); + $this->assertInstanceOf(Command::class, $command); + $commandString = $command->toString(); + $this->assertStringContainsString('fetch', $commandString); + $this->assertStringContainsString('--depth=1', $commandString); + $this->assertStringContainsString($commitHash, $commandString); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } @@ -608,14 +613,11 @@ public function testGenerateCloneCommandWithTag(): void '/' ); - // Verify the command contains tag-specific git commands - $this->assertIsString($command); - $this->assertStringContainsString('git init', $command); - $this->assertStringContainsString('git remote add origin', $command); - $this->assertStringContainsString('git config core.sparseCheckout true', $command); - $this->assertStringContainsString('refs/tags', $command); - $this->assertStringContainsString('v1.0.0', $command); - $this->assertStringContainsString('git checkout FETCH_HEAD', $command); + $this->assertInstanceOf(Command::class, $command); + $commandString = $command->toString(); + $this->assertStringContainsString('refs/tags', $commandString); + $this->assertStringContainsString('v1.0.0', $commandString); + $this->assertStringContainsString('FETCH_HEAD', $commandString); } finally { $this->vcsAdapter->deleteRepository(static::$owner, $repositoryName); } @@ -635,8 +637,11 @@ public function testGenerateCloneCommandWithInvalidRepository(): void '/' ); - $output = []; - exec($command . ' 2>&1', $output, $exitCode); + $this->assertInstanceOf(Command::class, $command); + + $output = ''; + $stderr = ''; + $exitCode = Console::execute($command, '', $output, $stderr, 30); $cloneFailed = ($exitCode !== 0) || !file_exists($directory . '/README.md');