From 83714ea395916edaf3adc522364dfd7e77e0f3f2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:33:45 +0200 Subject: [PATCH] fix(git): parse generic scp-style SSH URLs with custom users Centralize scp-style Git URL parsing so user@host:path (including custom usernames and embedded ports) is accepted and converted to HTTPS for public clones, API create, webhooks, validation, and commit/branch links. --- .../Api/ApplicationsController.php | 14 ++-- .../MatchesManualWebhookApplications.php | 9 +-- .../New/GithubPrivateRepositoryDeployKey.php | 8 +++ .../Project/New/PublicGitRepository.php | 5 +- app/Models/Application.php | 44 +++++++----- app/Rules/ValidGitRepositoryUrl.php | 5 +- bootstrap/helpers/shared.php | 65 +++++++++++++++-- .../Api/PublicApplicationSshUrlApiTest.php | 53 ++++++++++++++ .../Feature/GitHttpTransportCommandsTest.php | 22 ++++++ .../Feature/Helpers/ConvertingGitUrlsTest.php | 8 +++ .../Feature/PublicGitRepositorySshUrlTest.php | 34 +++++++++ tests/Feature/Webhook/WebhookHmacTest.php | 23 ++++++ tests/Unit/ApplicationGitCommitLinkTest.php | 39 +++++++++++ tests/Unit/ScpStyleGitUrlTest.php | 70 +++++++++++++++++++ tests/Unit/ValidGitRepositoryUrlTest.php | 3 + 15 files changed, 361 insertions(+), 41 deletions(-) create mode 100644 tests/Feature/Api/PublicApplicationSshUrlApiTest.php create mode 100644 tests/Feature/PublicGitRepositorySshUrlTest.php create mode 100644 tests/Unit/ScpStyleGitUrlTest.php diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index dbe0f8633..67fd515bc 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1460,7 +1460,13 @@ private function create_application(Request $request, $type) $application->docker_compose_domains = json_encode($dockerComposeDomainsJson); $application->domain_port_overrides = $domainPortOverrides; } - $repository_url_parsed = Url::fromString($request->git_repository); + $gitRepository = $application->git_repository; + $httpsRepository = scpStyleGitUrlToHttps($gitRepository); + if (is_string($httpsRepository)) { + $gitRepository = $httpsRepository; + $application->git_repository = $httpsRepository; + } + $repository_url_parsed = Url::fromString($gitRepository); $git_host = $repository_url_parsed->getHost(); if ($git_host === 'github.com') { $application->source_type = GithubApp::class; @@ -1622,11 +1628,7 @@ private function create_application(Request $request, $type) return response()->json(['message' => 'Failed to generate Github App token.'], 400); } - $gitRepository = $request->git_repository; - if (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { - $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); - } - $gitRepository = str($gitRepository)->trim('/')->replaceEnd('.git', '')->toString(); + $gitRepository = gitRepositorySlug($request->git_repository); // Use direct API call to verify repository access instead of loading all repositories // This is much faster and avoids timeouts for GitHub Apps with many repositories diff --git a/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php b/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php index 9fb3bb2de..65c92f134 100644 --- a/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php +++ b/app/Http/Controllers/Webhook/Concerns/MatchesManualWebhookApplications.php @@ -5,7 +5,6 @@ use App\Models\Application; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; -use Illuminate\Support\Str; trait MatchesManualWebhookApplications { @@ -79,12 +78,8 @@ protected function canonicalManualWebhookRepository(?string $gitRepository): ?st if (is_array($parts) && isset($parts['scheme'])) { $path = data_get($parts, 'path'); - } elseif (preg_match('/^[A-Za-z0-9._-]+@[^:]+:/', $gitRepository) === 1) { - $path = Str::after($gitRepository, ':'); - // scp-style SSH URLs embed a custom port as "user@host:2222/owner/repo". - // Strip the leading numeric port segment so the path matches the webhook - // payload's owner/repo, consistent with convertGitUrl() in shared.php. - $path = preg_replace('#^\d+/#', '', $path) ?? $path; + } elseif (($scp = parseScpStyleGitUrl($gitRepository)) !== null) { + $path = $scp['path']; } else { $path = $gitRepository; } diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 502a69bec..98c5395bf 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -216,6 +216,14 @@ private function get_git_source() throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url')); } + if (($scp = parseScpStyleGitUrl($this->repository_url)) !== null) { + $this->git_host = $scp['host']; + $this->git_repository = $this->repository_url; + $this->git_source = 'other'; + + return; + } + $this->repository_url_parsed = Url::fromString($this->repository_url); $this->git_host = $this->repository_url_parsed->getHost(); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 1ef1855c1..a031c50c0 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -137,8 +137,9 @@ public function loadBranch() throw new \RuntimeException('Invalid repository URL: '.$validator->errors()->first('repository_url')); } - if (preg_match('/^(?[A-Za-z0-9._-]+)@(?[^:]+):(?.+)$/', $this->repository_url, $matches) === 1) { - $this->repository_url = 'https://'.$matches['host'].'/'.$matches['repository']; + $httpsRepositoryUrl = scpStyleGitUrlToHttps($this->repository_url); + if (is_string($httpsRepositoryUrl)) { + $this->repository_url = $httpsRepositoryUrl; } if ( (str($this->repository_url)->startsWith('https://') || diff --git a/app/Models/Application.php b/app/Models/Application.php index cec8d501a..2d46291c5 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -663,15 +663,13 @@ public function gitBranchLocation(): Attribute return "{$this->source->html_url}/{$this->git_repository}/tree/{$this->git_branch}{$base_dir}"; } - // Convert the SSH URL to HTTPS URL - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - + $httpsRepository = $this->httpsUrlFromScpStyleGitRepository(); + if (is_string($httpsRepository)) { if (str($this->git_repository)->contains('bitbucket')) { - return "https://{$git_repository}/src/{$this->git_branch}{$base_dir}"; + return "{$httpsRepository}/src/{$this->git_branch}{$base_dir}"; } - return "https://{$git_repository}/tree/{$this->git_branch}{$base_dir}"; + return "{$httpsRepository}/tree/{$this->git_branch}{$base_dir}"; } return $this->git_repository; @@ -686,11 +684,9 @@ public function gitWebhook(): Attribute if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/settings/hooks"; } - // Convert the SSH URL to HTTPS URL - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - - return "https://{$git_repository}/settings/hooks"; + $httpsRepository = $this->httpsUrlFromScpStyleGitRepository(); + if (is_string($httpsRepository)) { + return "{$httpsRepository}/settings/hooks"; } return $this->git_repository; @@ -705,11 +701,9 @@ public function gitCommits(): Attribute if (! is_null($this->source?->html_url) && ! is_null($this->git_repository) && ! is_null($this->git_branch)) { return "{$this->source->html_url}/{$this->git_repository}/commits/{$this->git_branch}"; } - // Convert the SSH URL to HTTPS URL - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = str_replace(['git@', ':', '.git'], ['', '/', ''], $this->git_repository); - - return "https://{$git_repository}/commits/{$this->git_branch}"; + $httpsRepository = $this->httpsUrlFromScpStyleGitRepository(); + if (is_string($httpsRepository)) { + return "{$httpsRepository}/commits/{$this->git_branch}"; } return $this->git_repository; @@ -728,8 +722,9 @@ public function gitCommitLink($link): ?string } $git_repository = $this->git_repository; - if (strpos($this->git_repository, 'git@') === 0) { - $git_repository = preg_replace('/^git@([^:]+):/', 'https://$1/', $git_repository); + $httpsRepository = scpStyleGitUrlToHttps($git_repository); + if (is_string($httpsRepository)) { + $git_repository = $httpsRepository; } elseif (str($this->git_repository)->startsWith('ssh://')) { $git_repository = 'https://'.parse_url($git_repository, PHP_URL_HOST).parse_url($git_repository, PHP_URL_PATH); } @@ -746,6 +741,17 @@ public function gitCommitLink($link): ?string return $url->__toString(); } + private function httpsUrlFromScpStyleGitRepository(): ?string + { + $httpsRepository = scpStyleGitUrlToHttps($this->git_repository); + + if (! is_string($httpsRepository)) { + return null; + } + + return Str::replaceEnd('.git', '', $httpsRepository); + } + public function dockerfileLocation(): Attribute { return Attribute::make( @@ -1477,7 +1483,7 @@ public function setGitImportSettings(string $deployment_uuid, string $git_clone_ // Check if .gitmodules file exists before running submodule commands $git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && if [ -f .gitmodules ]; then"; if ($public) { - $git_clone_command = "{$git_clone_command} sed -i \"s#git@\(.*\):#https://\\1/#g\" {$escapedBaseDir}/.gitmodules || true &&"; + $git_clone_command = "{$git_clone_command} sed -i \"s#[A-Za-z0-9._-]*@\(.*\):#https://\\1/#g\" {$escapedBaseDir}/.gitmodules || true &&"; } // Add shallow submodules flag if shallow clone is enabled $submoduleFlags = $isShallowCloneEnabled ? '--depth=1' : ''; diff --git a/app/Rules/ValidGitRepositoryUrl.php b/app/Rules/ValidGitRepositoryUrl.php index 7cccfa5e9..29e219bd3 100644 --- a/app/Rules/ValidGitRepositoryUrl.php +++ b/app/Rules/ValidGitRepositoryUrl.php @@ -85,7 +85,8 @@ public function validate(string $attribute, mixed $value, Closure $fail): void } // Validate scp-style SSH URL format (user@host:user/repo.git) - if (! preg_match('/^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+:[a-zA-Z0-9\-_\/.~]+$/', $value)) { + $scp = parseScpStyleGitUrl($value); + if ($scp === null || preg_match('/^[a-zA-Z0-9.-]+$/', $scp['host']) !== 1 || preg_match('/^[a-zA-Z0-9\-_\/.~]+$/', $scp['path']) !== 1) { $fail('The :attribute is not a valid SSH repository URL.'); return; @@ -149,7 +150,7 @@ public function validate(string $attribute, mixed $value, Closure $fail): void return; } } else { - $fail('The :attribute must start with https://, http://, git://, or git@.'); + $fail('The :attribute must start with https://, http://, git://, or be an SSH URL (user@host:path).'); return; } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index 5c9516b79..d1f5e4016 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -4358,6 +4358,62 @@ function defaultNginxConfiguration(string $type = 'static'): string } } +/** + * Parse an scp-style SSH Git URL (`user@host:path` or `user@host:port/path`). + * + * @return array{user: string, host: string, port: ?string, path: string}|null + */ +function parseScpStyleGitUrl(?string $gitRepository): ?array +{ + if (! is_string($gitRepository) || $gitRepository === '') { + return null; + } + + if (preg_match('/^(?[A-Za-z0-9._-]+)@(?[^:]+):(?:(?\d+)\/)?(?.+)$/', $gitRepository, $matches) !== 1) { + return null; + } + + $host = trim($matches['host']); + $path = ltrim($matches['path'], '/'); + + if ($host === '' || $path === '') { + return null; + } + + return [ + 'user' => $matches['user'], + 'host' => $host, + 'port' => ($matches['port'] ?? '') === '' ? null : $matches['port'], + 'path' => $path, + ]; +} + +function scpStyleGitUrlToHttps(?string $gitRepository): ?string +{ + $parts = parseScpStyleGitUrl($gitRepository); + + if ($parts === null) { + return null; + } + + return 'https://'.$parts['host'].'/'.$parts['path']; +} + +function gitRepositorySlug(?string $gitRepository): string +{ + if (! is_string($gitRepository) || $gitRepository === '') { + return ''; + } + + if (($scp = parseScpStyleGitUrl($gitRepository)) !== null) { + $gitRepository = $scp['path']; + } elseif (str($gitRepository)->startsWith('http') || str($gitRepository)->contains('github.com')) { + $gitRepository = str($gitRepository)->replace('https://', '')->replace('http://', '')->replace('github.com/', ''); + } + + return str($gitRepository)->trim('/')->replaceEnd('.git', '')->toString(); +} + function convertGitUrl(string $gitRepository, string $deploymentType, GithubApp|GitlabApp|null $source = null): array { $repository = $gitRepository; @@ -4368,7 +4424,6 @@ function convertGitUrl(string $gitRepository, string $deploymentType, GithubApp| 'repository' => $gitRepository, ]; $sshMatches = []; - $matches = []; // Let's try and parse the string to detect if it's a valid SSH string or not preg_match('/((.*?)\:\/\/)?(.*@.*:.*)/', $gitRepository, $sshMatches); @@ -4403,11 +4458,11 @@ function convertGitUrl(string $gitRepository, string $deploymentType, GithubApp| $providerInfo['port'] = (string) $parsedRepository['port']; } } else { - preg_match('/^(?[^:]+):(?\d+)\/(?.+)$/', $normalizedRepository, $matches); + $scp = parseScpStyleGitUrl($normalizedRepository); - if (! empty($matches['port'])) { - $providerInfo['port'] = $matches['port']; - $repository = "{$matches['host']}:{$matches['path']}"; + if ($scp !== null && $scp['port'] !== null) { + $providerInfo['port'] = $scp['port']; + $repository = "{$scp['user']}@{$scp['host']}:{$scp['path']}"; } } diff --git a/tests/Feature/Api/PublicApplicationSshUrlApiTest.php b/tests/Feature/Api/PublicApplicationSshUrlApiTest.php new file mode 100644 index 000000000..03c18ab48 --- /dev/null +++ b/tests/Feature/Api/PublicApplicationSshUrlApiTest.php @@ -0,0 +1,53 @@ + 'file']); + Storage::fake('ssh-keys'); + InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0])); + + $this->team = Team::factory()->create(); + $this->user = User::factory()->create(); + $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); + + $this->bearerToken = $this->user->createToken('public-ssh-url-api-test', ['*'])->plainTextToken; + $this->server = Server::factory()->create(['team_id' => $this->team->id]); + $this->destination = StandaloneDocker::where('server_id', $this->server->id)->first(); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); +}); + +test('public application api converts scp-style ssh urls to https', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + 'Content-Type' => 'application/json', + ])->postJson('/api/v1/applications/public', [ + 'project_uuid' => $this->project->uuid, + 'environment_uuid' => $this->environment->uuid, + 'server_uuid' => $this->server->uuid, + 'git_repository' => 'custom-user@git.example.com:2222/organization/repository.git', + 'git_branch' => 'main', + 'build_pack' => 'nixpacks', + 'ports_exposes' => '3000', + 'autogenerate_domain' => false, + ]); + + $response->assertCreated(); + + $application = Application::where('uuid', $response->json('uuid'))->firstOrFail(); + + expect($application->git_repository)->toBe('https://git.example.com/organization/repository.git'); +}); diff --git a/tests/Feature/GitHttpTransportCommandsTest.php b/tests/Feature/GitHttpTransportCommandsTest.php index d6f9f1337..3d0295d07 100644 --- a/tests/Feature/GitHttpTransportCommandsTest.php +++ b/tests/Feature/GitHttpTransportCommandsTest.php @@ -69,6 +69,28 @@ function applicationWithGitSettings(bool $shallow = true): Application ->toContain("git -c http.version=HTTP/1.1 -c advice.detachedHead=false checkout 'abc123def456abc123def456abc123def456abc1'"); }); +it('rewrites generic ssh submodule remotes to https for public clones', function () { + $application = applicationWithGitSettings(shallow: false); + $application->settings->is_git_submodules_enabled = true; + + $source = new GithubApp; + $source->forceFill([ + 'html_url' => 'https://github.com', + 'api_url' => 'https://api.github.com', + 'is_public' => true, + ]); + $application->setRelation('source', $source); + + $result = $application->generateGitImportCommands( + deployment_uuid: 'test-deployment', + exec_in_docker: false, + ); + + expect($result['commands']) + ->toContain('sed -i "s#[A-Za-z0-9._-]*@\(.*\):#https://\\1/#g"') + ->not->toContain('s#git@\(.*\):#https://\\1/#g'); +}); + it('does not add http transport config to ssh deploy key clones', function () { $application = applicationWithGitSettings(); $application->private_key_id = 1; diff --git a/tests/Feature/Helpers/ConvertingGitUrlsTest.php b/tests/Feature/Helpers/ConvertingGitUrlsTest.php index 96b19fcc9..1860eceed 100644 --- a/tests/Feature/Helpers/ConvertingGitUrlsTest.php +++ b/tests/Feature/Helpers/ConvertingGitUrlsTest.php @@ -61,6 +61,14 @@ ]); }); +test('convertGitUrlsForSourceAndSshUrlWithCustomUsernameAndPort', function () { + $result = convertGitUrl('custom-user@git.domain.com:766/group/project.git', 'source', null); + expect($result)->toBe([ + 'repository' => 'custom-user@git.domain.com:group/project.git', + 'port' => '766', + ]); +}); + test('convertGitUrlsForSourceAndSshUrlSchemeWithCustomPort', function () { $result = convertGitUrl('ssh://git@192.168.56.11:22222/User/Repo.git', 'source', null); expect($result)->toBe([ diff --git a/tests/Feature/PublicGitRepositorySshUrlTest.php b/tests/Feature/PublicGitRepositorySshUrlTest.php new file mode 100644 index 000000000..80df73a9f --- /dev/null +++ b/tests/Feature/PublicGitRepositorySshUrlTest.php @@ -0,0 +1,34 @@ +create(); + $user = User::factory()->create(); + $team->members()->attach($user->id, ['role' => 'owner']); + + $this->actingAs($user); + session(['currentTeam' => $team]); +}); + +test('converts scp-style ssh urls with custom usernames to https', function () { + Livewire::test(PublicGitRepository::class, ['type' => 'public']) + ->set('repository_url', 'custom-user@git.example.com:organization/repository.git') + ->call('loadBranch') + ->assertSet('repository_url', 'https://git.example.com/organization/repository.git') + ->assertSet('branchFound', true); +}); + +test('strips custom ports when converting scp-style ssh urls to https', function () { + Livewire::test(PublicGitRepository::class, ['type' => 'public']) + ->set('repository_url', 'custom-user@git.example.com:2222/organization/repository.git') + ->call('loadBranch') + ->assertSet('repository_url', 'https://git.example.com/organization/repository.git') + ->assertSet('branchFound', true); +}); diff --git a/tests/Feature/Webhook/WebhookHmacTest.php b/tests/Feature/Webhook/WebhookHmacTest.php index 45e0da377..dfd799018 100644 --- a/tests/Feature/Webhook/WebhookHmacTest.php +++ b/tests/Feature/Webhook/WebhookHmacTest.php @@ -565,6 +565,29 @@ function createApplicationWithWebhook(string $repo = 'test-org/test-repo', strin expect($response->getContent())->not->toContain('No applications found'); }); + test('github matches an ssh repository URL with a non-git username and custom port', function () { + $app = createApplicationWithWebhook(overrides: [ + 'git_repository' => 'custom-user@git.example.com:2222/test-org/test-repo.git', + ]); + $secret = $app->manual_webhook_secret_github; + + $payload = json_encode([ + 'ref' => 'refs/heads/main', + 'repository' => ['full_name' => 'test-org/test-repo'], + 'after' => 'abc123', + 'commits' => [], + ]); + + $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ + 'HTTP_X-GitHub-Event' => 'push', + 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret), + 'CONTENT_TYPE' => 'application/json', + ], $payload); + + $response->assertOk(); + expect($response->getContent())->not->toContain('No applications found'); + }); + test('gitlab matches scp-style ssh repository URL with custom port', function () { $app = createApplicationWithWebhook(overrides: [ 'git_repository' => 'git@gitlab.example.com:2222/services/xyz.git', diff --git a/tests/Unit/ApplicationGitCommitLinkTest.php b/tests/Unit/ApplicationGitCommitLinkTest.php index 378384fe8..6ebbaf8ee 100644 --- a/tests/Unit/ApplicationGitCommitLinkTest.php +++ b/tests/Unit/ApplicationGitCommitLinkTest.php @@ -17,6 +17,14 @@ 'git@github.com:coollabsio/coolify.git', 'https://github.com/coollabsio/coolify/commit/1234567890abcdef', ], + 'SSH remote with custom username' => [ + 'custom-user@git.example.com:coollabsio/coolify.git', + 'https://git.example.com/coollabsio/coolify/commit/1234567890abcdef', + ], + 'SSH remote with custom username and port' => [ + 'custom-user@git.example.com:2222/coollabsio/coolify.git', + 'https://git.example.com/coollabsio/coolify/commit/1234567890abcdef', + ], 'SSH URL' => [ 'ssh://git@gitlab.com/coollabsio/coolify.git', 'https://gitlab.com/coollabsio/coolify/commit/1234567890abcdef', @@ -37,3 +45,34 @@ 'missing host' => 'https://', 'missing scheme' => 'github.com/coollabsio/coolify', ]); + +it('converts scp-style remotes with generic usernames into https repository links', function (string $repository, string $expectedBranch, string $expectedCommits, string $expectedWebhook) { + $application = new Application; + $application->setRelation('source', null); + $application->git_repository = $repository; + $application->git_branch = 'main'; + $application->base_directory = '/'; + + expect($application->gitBranchLocation)->toBe($expectedBranch) + ->and($application->gitCommits)->toBe($expectedCommits) + ->and($application->gitWebhook)->toBe($expectedWebhook); +})->with([ + 'git username' => [ + 'git@github.com:coollabsio/coolify.git', + 'https://github.com/coollabsio/coolify/tree/main/', + 'https://github.com/coollabsio/coolify/commits/main', + 'https://github.com/coollabsio/coolify/settings/hooks', + ], + 'custom username' => [ + 'custom-user@git.example.com:organization/repository.git', + 'https://git.example.com/organization/repository/tree/main/', + 'https://git.example.com/organization/repository/commits/main', + 'https://git.example.com/organization/repository/settings/hooks', + ], + 'custom username and port' => [ + 'custom-user@git.example.com:2222/organization/repository.git', + 'https://git.example.com/organization/repository/tree/main/', + 'https://git.example.com/organization/repository/commits/main', + 'https://git.example.com/organization/repository/settings/hooks', + ], +]); diff --git a/tests/Unit/ScpStyleGitUrlTest.php b/tests/Unit/ScpStyleGitUrlTest.php new file mode 100644 index 000000000..ec95e341c --- /dev/null +++ b/tests/Unit/ScpStyleGitUrlTest.php @@ -0,0 +1,70 @@ +toBe($expected); +})->with([ + 'git username' => [ + 'git@github.com:organization/repository.git', + [ + 'user' => 'git', + 'host' => 'github.com', + 'port' => null, + 'path' => 'organization/repository.git', + ], + ], + 'custom username' => [ + 'custom-user@git.example.com:organization/repository.git', + [ + 'user' => 'custom-user', + 'host' => 'git.example.com', + 'port' => null, + 'path' => 'organization/repository.git', + ], + ], + 'custom username and port' => [ + 'custom-user@git.example.com:2222/organization/repository.git', + [ + 'user' => 'custom-user', + 'host' => 'git.example.com', + 'port' => '2222', + 'path' => 'organization/repository.git', + ], + ], +]); + +it('converts scp-style ssh git urls to https without embedding custom ports in the path', function (string $url, string $expected) { + expect(scpStyleGitUrlToHttps($url))->toBe($expected); +})->with([ + 'git username' => [ + 'git@github.com:organization/repository.git', + 'https://github.com/organization/repository.git', + ], + 'custom username' => [ + 'custom-user@git.example.com:organization/repository.git', + 'https://git.example.com/organization/repository.git', + ], + 'custom username and port' => [ + 'custom-user@git.example.com:2222/organization/repository.git', + 'https://git.example.com/organization/repository.git', + ], +]); + +it('rejects non-scp-style git urls', function (string $url) { + expect(parseScpStyleGitUrl($url))->toBeNull() + ->and(scpStyleGitUrlToHttps($url))->toBeNull(); +})->with([ + 'https' => 'https://github.com/organization/repository.git', + 'email without path' => 'custom-user@git.example.com', + 'ssh scheme' => 'ssh://git@github.com/organization/repository.git', + 'empty' => '', +]); + +it('normalizes github app repository slugs from scp-style ssh urls', function (string $url, string $expected) { + expect(gitRepositorySlug($url))->toBe($expected); +})->with([ + 'https' => ['https://github.com/organization/repository.git', 'organization/repository'], + 'owner/repo' => ['organization/repository', 'organization/repository'], + 'git username' => ['git@github.com:organization/repository.git', 'organization/repository'], + 'custom username' => ['custom-user@git.example.com:organization/repository.git', 'organization/repository'], + 'custom username and port' => ['custom-user@git.example.com:2222/organization/repository.git', 'organization/repository'], +]); diff --git a/tests/Unit/ValidGitRepositoryUrlTest.php b/tests/Unit/ValidGitRepositoryUrlTest.php index ee57657a0..b4bcb465a 100644 --- a/tests/Unit/ValidGitRepositoryUrlTest.php +++ b/tests/Unit/ValidGitRepositoryUrlTest.php @@ -108,6 +108,8 @@ 'git@gitlab.com:user/repo.git', 'git@bitbucket.org:user/repo.git', 'custom-user@git.example.com:organization/repository.git', + 'custom-user@git.example.com:2222/organization/repository.git', + 'enterprise-user@enterprise.ghe.com:organization/repository.git', ]; foreach ($validUrls as $url) { @@ -130,6 +132,7 @@ $invalidUrls = [ 'git@github.com:user/repo.git', 'git@gitlab.com:user/repo.git', + 'custom-user@git.example.com:organization/repository.git', ]; foreach ($invalidUrls as $url) {