Skip to content

Commit 239d7c6

Browse files
feat: return structured clone commands from git adapters
1 parent 86e397d commit 239d7c6

9 files changed

Lines changed: 464 additions & 136 deletions

File tree

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"php": ">=8.0",
3131
"adhocore/jwt": "^1.1",
3232
"utopia-php/cache": "1.0.*",
33+
"utopia-php/console": "dev-main",
3334
"utopia-php/fetch": "0.5.*"
3435
},
3536
"require-dev": {

composer.lock

Lines changed: 99 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/VCS/Adapter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Utopia\VCS;
44

55
use Exception;
6+
use Utopia\Command;
67

78
abstract class Adapter
89
{
@@ -191,7 +192,7 @@ abstract public function updateComment(string $owner, string $repositoryName, in
191192
/**
192193
* Generates a clone command using app access token
193194
*/
194-
abstract public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string;
195+
abstract public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): Command;
195196

196197
/**
197198
* Parses webhook event payload

src/VCS/Adapter/Git/GitHub.php

Lines changed: 116 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Ahc\Jwt\JWT;
66
use Exception;
7+
use Utopia\Command;
78
use Utopia\Cache\Cache;
89
use Utopia\VCS\Adapter\Git;
910
use Utopia\VCS\Exception\FileNotFound;
@@ -841,7 +842,7 @@ public function updateCommitStatus(string $repositoryName, string $commitHash, s
841842
/**
842843
* Generates a clone command using app access token
843844
*/
844-
public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): string
845+
public function generateCloneCommand(string $owner, string $repositoryName, string $version, string $versionType, string $directory, string $rootDirectory): Command
845846
{
846847
if (empty($rootDirectory)) {
847848
$rootDirectory = '*';
@@ -854,42 +855,133 @@ public function generateCloneCommand(string $owner, string $repositoryName, stri
854855

855856
$cloneUrl = "https://{$owner}{$accessToken}@github.com/{$owner}/{$repositoryName}";
856857

857-
$directory = escapeshellarg($directory);
858-
$rootDirectory = escapeshellarg($rootDirectory);
859-
860858
$commands = [
861-
"mkdir -p {$directory}",
862-
"cd {$directory}",
863-
"git config --global init.defaultBranch main",
864-
"git init",
865-
"git remote add origin {$cloneUrl}",
866-
// Enable sparse checkout
867-
"git config core.sparseCheckout true",
868-
"echo {$rootDirectory} >> .git/info/sparse-checkout",
869-
// Disable fetching of refs we don't need
870-
"git config --add remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'",
871-
// Disable fetching of tags
872-
"git config remote.origin.tagopt --no-tags",
859+
(new Command('mkdir'))
860+
->flag('-p')
861+
->argument($directory),
862+
(new Command('git'))
863+
->argument('config')
864+
->argument('--global')
865+
->argument('init.defaultBranch')
866+
->argument('main'),
867+
(new Command('git'))
868+
->argument('init')
869+
->argument($directory),
870+
(new Command('git'))
871+
->option('-C', $directory)
872+
->argument('remote')
873+
->argument('add')
874+
->argument('origin')
875+
->argument($cloneUrl),
876+
(new Command('git'))
877+
->option('-C', $directory)
878+
->argument('config')
879+
->argument('--add')
880+
->argument('remote.origin.fetch')
881+
->argument('+refs/heads/*:refs/remotes/origin/*'),
882+
(new Command('git'))
883+
->option('-C', $directory)
884+
->argument('config')
885+
->argument('remote.origin.tagopt')
886+
->argument('--no-tags'),
887+
(new Command('git'))
888+
->option('-C', $directory)
889+
->argument('sparse-checkout')
890+
->argument('set')
891+
->argument('--no-cone')
892+
->argument($rootDirectory),
873893
];
874894

875895
switch ($versionType) {
876896
case self::CLONE_TYPE_BRANCH:
877-
$branchName = escapeshellarg($version);
878-
$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";
897+
$commands[] = Command::or(
898+
Command::and(
899+
(new Command('git'))
900+
->option('-C', $directory)
901+
->argument('ls-remote')
902+
->argument('--exit-code')
903+
->argument('--heads')
904+
->argument('origin')
905+
->argument($version),
906+
(new Command('git'))
907+
->option('-C', $directory)
908+
->argument('pull')
909+
->argument('--depth=1')
910+
->argument('origin')
911+
->argument($version),
912+
(new Command('git'))
913+
->option('-C', $directory)
914+
->argument('checkout')
915+
->argument($version)
916+
),
917+
(new Command('git'))
918+
->option('-C', $directory)
919+
->argument('checkout')
920+
->argument('-b')
921+
->argument($version)
922+
);
879923
break;
880924
case self::CLONE_TYPE_COMMIT:
881-
$commitHash = escapeshellarg($version);
882-
$commands[] = "git fetch --depth=1 origin {$commitHash} && git checkout {$commitHash}";
925+
$commands[] = (new Command('git'))
926+
->option('-C', $directory)
927+
->argument('fetch')
928+
->argument('--depth=1')
929+
->argument('origin')
930+
->argument($version);
931+
$commands[] = (new Command('git'))
932+
->option('-C', $directory)
933+
->argument('checkout')
934+
->argument($version);
883935
break;
884936
case self::CLONE_TYPE_TAG:
885-
$tagName = escapeshellarg($version);
886-
$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";
937+
$resolvedTag = $this->resolveTagReference($owner, $repositoryName, $version);
938+
$commands[] = (new Command('git'))
939+
->option('-C', $directory)
940+
->argument('fetch')
941+
->argument('--depth=1')
942+
->argument('origin')
943+
->argument('refs/tags/' . $resolvedTag);
944+
$commands[] = (new Command('git'))
945+
->option('-C', $directory)
946+
->argument('checkout')
947+
->argument('FETCH_HEAD');
887948
break;
888949
}
889950

890-
$fullCommand = implode(" && ", $commands);
951+
return Command::and(...$commands);
952+
}
953+
954+
private function resolveTagReference(string $owner, string $repositoryName, string $version): string
955+
{
956+
if (!str_contains($version, '*')) {
957+
return $version;
958+
}
959+
960+
$prefix = rtrim(strstr($version, '*', true) ?: '', '.');
961+
$refPrefix = 'tags' . (!empty($prefix) ? '/' . $prefix : '');
962+
$response = $this->call(
963+
self::METHOD_GET,
964+
"/repos/{$owner}/{$repositoryName}/git/matching-refs/{$refPrefix}",
965+
['Authorization' => "Bearer $this->accessToken"]
966+
);
967+
968+
$refs = $response['body'] ?? [];
969+
$matches = [];
970+
971+
foreach ($refs as $ref) {
972+
$tag = str_replace('refs/tags/', '', $ref['ref'] ?? '');
973+
if ($tag !== '' && fnmatch($version, $tag)) {
974+
$matches[] = $tag;
975+
}
976+
}
977+
978+
if (empty($matches)) {
979+
throw new Exception("Tag not found for pattern: {$version}");
980+
}
981+
982+
usort($matches, static fn (string $left, string $right): int => version_compare($left, $right));
891983

892-
return $fullCommand;
984+
return $matches[array_key_last($matches)];
893985
}
894986

895987
/**

0 commit comments

Comments
 (0)