, 2: list} */ function runImportCommandWithStubGit(string $lfsFiles, bool $hasSubmodules = false): array { $dir = sys_get_temp_dir().'/coolify-import-'.bin2hex(random_bytes(4)); mkdir("{$dir}/bin", 0755, true); file_put_contents("{$dir}/bin/git", <<> {$dir}/git.log if [ "\$*" = "lfs ls-files -n" ]; then printf '%s' '{$lfsFiles}'; fi SH); chmod("{$dir}/bin/git", 0755); if ($hasSubmodules) { touch("{$dir}/.gitmodules"); } $application = new Application; $application->forceFill(['uuid' => 'test-app-uuid', 'git_commit_sha' => 'abc123']); $settings = new ApplicationSetting; $settings->is_git_shallow_clone_enabled = true; $settings->is_git_submodules_enabled = true; $settings->is_git_lfs_enabled = true; $application->setRelation('settings', $settings); $command = $application->setGitImportSettings( deployment_uuid: 'test-uuid', git_clone_command: 'true', gitSshCommand: 'ssh -o ConnectTimeout=30 -i /root/.ssh/id_rsa', baseDir: $dir, ); $process = Process::fromShellCommandline($command, env: ['PATH' => "{$dir}/bin:".getenv('PATH')]); $process->run(); $calls = file_exists("{$dir}/git.log") ? file("{$dir}/git.log", FILE_IGNORE_NEW_LINES) : []; exec('rm -rf '.escapeshellarg($dir)); return [$process->getExitCode(), $calls, explode("\n", trim($process->getOutput()))]; } test('git lfs pull is skipped when the checkout has no LFS files', function () { [$exitCode, $calls, $output] = runImportCommandWithStubGit(''); expect($exitCode)->toBe(0) ->and($calls)->toBe([ 'fetch --depth=1 origin abc123', '-c advice.detachedHead=false checkout abc123', 'lfs ls-files -n', ]) ->and($output)->toBe(['Fetching the commit.']); }); test('each network step after the clone prints a log line before it runs', function () { [$exitCode, $calls, $output] = runImportCommandWithStubGit('assets/logo.psd', hasSubmodules: true); expect($exitCode)->toBe(0) ->and($calls)->toBe([ 'fetch --depth=1 origin abc123', '-c advice.detachedHead=false checkout abc123', 'submodule sync', 'submodule update --init --recursive --depth=1', 'lfs ls-files -n', 'lfs pull', ]) ->and($output)->toBe(['Fetching the commit.', 'Updating submodules.', 'Downloading Git LFS objects.']); }); test('git ssh command stops a stalled connection instead of waiting for the deployment timeout', function () { expect(gitSshCommand('test-uuid', 2222, '/root/.ssh/key'))->toBe( 'ssh -F /root/.ssh/coolify_git_test-uuid.config -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -p 2222 -o Port=2222 -o LogLevel=ERROR -i /root/.ssh/key -o IdentitiesOnly=yes' ) ->and(gitSshCommand('test-uuid', 22))->not->toContain(' -i ') ->and(gitSshCommand('test-uuid', 22, '/root/.ssh/id_rsa', identitiesOnly: false))->toEndWith('-i /root/.ssh/id_rsa'); }); test('a shallow clone checks out a short commit sha', function (string $commitName) { $dir = sys_get_temp_dir().'/coolify-short-sha-'.bin2hex(random_bytes(4)); $git = 'git -c user.email=test@example.com -c user.name=test'; exec("git init -q -b main {$dir}/source && cd {$dir}/source && {$git} commit -q --allow-empty -m main" ." && git checkout -q -b feature && {$git} commit -q --allow-empty -m first && {$git} commit -q --allow-empty -m second" ." && git checkout -q main && git clone -q --bare {$dir}/source {$dir}/remote.git"); $commit = trim(shell_exec("git -C {$dir}/source rev-parse {$commitName}")); $application = new Application; $application->forceFill(['uuid' => 'test-app-uuid']); $settings = new ApplicationSetting; $settings->is_git_shallow_clone_enabled = true; $settings->is_git_submodules_enabled = false; $settings->is_git_lfs_enabled = false; $application->setRelation('settings', $settings); // Bitbucket sends the pull request commit as a 12-character hash. $command = $application->setGitImportSettings( deployment_uuid: 'test-uuid', git_clone_command: "git clone -q --depth=1 -b main file://{$dir}/remote.git {$dir}/checkout", commit: substr($commit, 0, 12), baseDir: "{$dir}/checkout", ); $process = Process::fromShellCommandline($command); $process->run(); $checkedOutCommit = trim((string) shell_exec("git -C {$dir}/checkout rev-parse HEAD 2>/dev/null")); exec('rm -rf '.escapeshellarg($dir)); expect($process->getExitCode())->toBe(0, $process->getErrorOutput()) ->and($checkedOutCommit)->toBe($commit); })->with([ 'tip of a branch' => ['feature'], 'older commit of a branch' => ['feature~1'], ]);