diff --git a/app/Models/Application.php b/app/Models/Application.php index e729d77fcc..9ab89e8442 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -1580,6 +1580,8 @@ class Application extends BaseModel : 'GIT_SSH_COMMAND="'.$resolvedGitSshCommand.'"') : 'GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"'; + $git_clone_command = $this->gitLfsSkipSmudgePrefix($onlyCheckout).$git_clone_command; + // Use the explicitly passed commit (e.g. from rollback), falling back to the application's git_commit_sha. // Invalid refs will cause the git checkout/fetch command to fail on the remote server. $commitToUse = $commit ?? $this->git_commit_sha; @@ -1809,6 +1811,16 @@ class Application extends BaseModel ]; } + /** + * The helper image installs the Git LFS filter system-wide, so every checkout downloads + * LFS objects unless the smudge filter is skipped. A checkout that only reads files (the + * Compose file) never needs LFS objects. + */ + private function gitLfsSkipSmudgePrefix(bool $onlyCheckout = false): string + { + return $this->settings->is_git_lfs_enabled && ! $onlyCheckout ? '' : 'export GIT_LFS_SKIP_SMUDGE=1 && '; + } + private function gitCommandsAsShellCommand(Collection|array|string $commands): string { if (is_string($commands)) { @@ -1922,10 +1934,11 @@ class Application extends BaseModel $git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name, gitConfigOptions: $gitConfigOption ?? null); $escapedPrBranch = escapeshellarg($branch); + $git_pr_command = "{$this->gitLfsSkipSmudgePrefix($only_checkout)}cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command"; if ($exec_in_docker) { - $commands->push($this->gitCommand(executeInDocker($deployment_uuid, "cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command"))); + $commands->push($this->gitCommand(executeInDocker($deployment_uuid, $git_pr_command))); } else { - $commands->push($this->gitCommand("cd {$escapedBaseDir} && git fetch origin {$escapedPrBranch} && $git_checkout_command")); + $commands->push($this->gitCommand($git_pr_command)); } } diff --git a/tests/Unit/GitLfsSkipSmudgeTest.php b/tests/Unit/GitLfsSkipSmudgeTest.php new file mode 100644 index 0000000000..81805d29b0 --- /dev/null +++ b/tests/Unit/GitLfsSkipSmudgeTest.php @@ -0,0 +1,92 @@ + + */ +function gitCallsWithLfsSkipSmudge(bool $isGitLfsEnabled, int $pullRequestId = 0, bool $onlyCheckout = false, string $commit = 'HEAD'): array +{ + $application = new Application; + $application->forceFill([ + 'uuid' => 'test-app-uuid', + 'git_repository' => 'duilio/bad-lfs-example', + 'git_branch' => 'main', + 'git_commit_sha' => $commit, + ]); + + $settings = new ApplicationSetting; + $settings->is_git_shallow_clone_enabled = false; + $settings->is_git_submodules_enabled = false; + $settings->is_git_lfs_enabled = $isGitLfsEnabled; + $application->setRelation('settings', $settings); + + $source = new GithubApp; + $source->forceFill(['html_url' => 'https://github.com', 'is_public' => true]); + $application->setRelation('source', $source); + + $bin = sys_get_temp_dir().'/coolify-lfs-test-'.uniqid(); + mkdir("{$bin}/repo", recursive: true); + + ['commands' => $commands] = $application->generateGitImportCommands( + deployment_uuid: 'test-deployment', + pull_request_id: $pullRequestId, + only_checkout: $onlyCheckout, + custom_base_dir: "{$bin}/repo", + ); + + file_put_contents("{$bin}/git", "#!/bin/bash\necho \"\${GIT_LFS_SKIP_SMUDGE:-unset} \$*\" >> \"\$GIT_CALL_LOG\"\n"); + file_put_contents("{$bin}/docker", "#!/bin/bash\nshift 4\nexec env -u GIT_LFS_SKIP_SMUDGE bash -c \"\$1\"\n"); + chmod("{$bin}/git", 0755); + chmod("{$bin}/docker", 0755); + + try { + Process::env(['PATH' => "{$bin}:".getenv('PATH'), 'GIT_CALL_LOG' => "{$bin}/calls.log"]) + ->run(['bash', '-ec', $commands]) + ->throw(); + + return str_replace($bin, '', file("{$bin}/calls.log", FILE_IGNORE_NEW_LINES)); + } finally { + File::deleteDirectory($bin); + } +} + +it('skips the lfs smudge filter for every git call when git lfs is disabled', function () { + $calls = gitCallsWithLfsSkipSmudge(isGitLfsEnabled: false, pullRequestId: 7); + + expect($calls)->toBe([ + '1 clone -b main https://github.com/duilio/bad-lfs-example /repo', + '1 fetch origin pull/7/head:pr-7-coolify', + '1 checkout pr-7-coolify', + ]); +}); + +it('keeps the lfs smudge filter and pulls lfs objects when git lfs is enabled', function () { + $calls = gitCallsWithLfsSkipSmudge(isGitLfsEnabled: true); + + expect($calls)->toBe([ + 'unset clone -b main https://github.com/duilio/bad-lfs-example /repo', + 'unset lfs ls-files -n', + ]); +}); + +it('skips the lfs smudge filter for the compose file checkout of a pinned commit when git lfs is enabled', function () { + $calls = gitCallsWithLfsSkipSmudge(isGitLfsEnabled: true, onlyCheckout: true, commit: 'abc123def456abc123def456abc123def456abc1'); + + expect($calls)->toBe([ + '1 clone --no-checkout -b main https://github.com/duilio/bad-lfs-example /repo', + '1 -c advice.detachedHead=false checkout abc123def456abc123def456abc123def456abc1', + ]); +});