Skip to content

Commit 96f8102

Browse files
committed
[#2162] Added repository and reference existence validation.
- Added validateRemoteRepositoryExists() to verify remote repositories are accessible - Added validateRemoteRefExists() to verify remote refs exist via HEAD requests - Added validateLocalRepositoryExists() to verify local paths are valid git repositories - Added validateLocalRefExists() to verify local refs exist using git rev-parse - Validation occurs before download attempts for better error messaging - Updated tests to expect new validation error messages - Special refs (stable, HEAD) skip reference validation as they are auto-discovered
1 parent a726150 commit 96f8102

2 files changed

Lines changed: 130 additions & 4 deletions

File tree

.vortex/installer/src/Downloader/RepositoryDownloader.php

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,9 @@ protected function downloadFromRemote(string $repo, string $ref, ?string $destin
161161
}
162162
$repo_url = str_ends_with($repo, '.git') ? substr($repo, 0, -4) : $repo;
163163

164+
// Validate repository exists before proceeding.
165+
$this->validateRemoteRepositoryExists($repo_url);
166+
164167
$version = $ref;
165168
if ($ref === RepositoryDownloader::REF_STABLE) {
166169
$ref = $this->discoverLatestReleaseRemote($repo_url);
@@ -174,6 +177,10 @@ protected function downloadFromRemote(string $repo, string $ref, ?string $destin
174177
elseif ($ref === RepositoryDownloader::REF_HEAD) {
175178
$version = 'develop';
176179
}
180+
else {
181+
// Validate ref exists for non-special refs.
182+
$this->validateRemoteRefExists($repo_url, $ref);
183+
}
177184

178185
$url = sprintf('%s/archive/%s.tar.gz', $repo_url, $ref);
179186

@@ -190,6 +197,9 @@ protected function downloadFromLocal(string $repo, string $ref, ?string $destina
190197
throw new \InvalidArgumentException('Destination cannot be null for local downloads.');
191198
}
192199

200+
// Validate local repository exists.
201+
$this->validateLocalRepositoryExists($repo);
202+
193203
$ref = $ref === RepositoryDownloader::REF_STABLE ? RepositoryDownloader::REF_HEAD : $ref;
194204
$version = $ref;
195205

@@ -200,6 +210,10 @@ protected function downloadFromLocal(string $repo, string $ref, ?string $destina
200210
$ref = $this->git->getLastShortCommitId();
201211
$version = 'develop';
202212
}
213+
else {
214+
// Validate ref exists for non-HEAD refs.
215+
$this->validateLocalRefExists($repo, $ref);
216+
}
203217

204218
$archive_path = $this->archiveFromLocal($repo, $ref);
205219
$this->archiver->validate($archive_path);
@@ -329,4 +343,116 @@ protected function archiveFromLocal(string $repo, string $ref): string {
329343
return $temp_file;
330344
}
331345

346+
/**
347+
* Validate that a remote repository exists and is accessible.
348+
*
349+
* @param string $repo_url
350+
* The repository URL (without .git extension).
351+
*
352+
* @throws \RuntimeException
353+
* If the repository is not accessible.
354+
*/
355+
protected function validateRemoteRepositoryExists(string $repo_url): void {
356+
$headers = ['User-Agent' => 'Vortex-Installer'];
357+
358+
$github_token = Env::get('GITHUB_TOKEN');
359+
if ($github_token) {
360+
$headers['Authorization'] = sprintf('Bearer %s', $github_token);
361+
}
362+
363+
try {
364+
// Try to access the repository root to verify it exists.
365+
$response = $this->httpClient->request('HEAD', $repo_url, ['headers' => $headers, 'http_errors' => FALSE]);
366+
$status_code = $response->getStatusCode();
367+
368+
if ($status_code >= 400) {
369+
throw new \RuntimeException(sprintf('Repository not found or not accessible: "%s" (HTTP %d)', $repo_url, $status_code));
370+
}
371+
}
372+
catch (RequestException $e) {
373+
throw new \RuntimeException(sprintf('Unable to access repository: "%s" - %s', $repo_url, $e->getMessage()), $e->getCode(), $e);
374+
}
375+
}
376+
377+
/**
378+
* Validate that a reference exists in a remote repository.
379+
*
380+
* @param string $repo_url
381+
* The repository URL (without .git extension).
382+
* @param string $ref
383+
* The git reference to validate.
384+
*
385+
* @throws \RuntimeException
386+
* If the reference does not exist.
387+
*/
388+
protected function validateRemoteRefExists(string $repo_url, string $ref): void {
389+
$archive_url = sprintf('%s/archive/%s.tar.gz', $repo_url, $ref);
390+
$headers = ['User-Agent' => 'Vortex-Installer'];
391+
392+
$github_token = Env::get('GITHUB_TOKEN');
393+
if ($github_token) {
394+
$headers['Authorization'] = sprintf('Bearer %s', $github_token);
395+
}
396+
397+
try {
398+
// Use HEAD request to check if the archive URL exists without downloading.
399+
$response = $this->httpClient->request('HEAD', $archive_url, ['headers' => $headers, 'http_errors' => FALSE]);
400+
$status_code = $response->getStatusCode();
401+
402+
if ($status_code === 404) {
403+
throw new \RuntimeException(sprintf('Reference "%s" not found in repository "%s"', $ref, $repo_url));
404+
}
405+
elseif ($status_code >= 400) {
406+
throw new \RuntimeException(sprintf('Unable to verify reference "%s" in repository "%s" (HTTP %d)', $ref, $repo_url, $status_code));
407+
}
408+
}
409+
catch (RequestException $e) {
410+
throw new \RuntimeException(sprintf('Unable to verify reference "%s" in repository "%s" - %s', $ref, $repo_url, $e->getMessage()), $e->getCode(), $e);
411+
}
412+
}
413+
414+
/**
415+
* Validate that a local repository exists and is a valid git repository.
416+
*
417+
* @param string $repo
418+
* The local repository path.
419+
*
420+
* @throws \RuntimeException
421+
* If the repository does not exist or is not a valid git repository.
422+
*/
423+
protected function validateLocalRepositoryExists(string $repo): void {
424+
if (!is_dir($repo)) {
425+
throw new \RuntimeException(sprintf('Local repository path does not exist: "%s"', $repo));
426+
}
427+
428+
if (!is_dir($repo . '/.git')) {
429+
throw new \RuntimeException(sprintf('Path is not a git repository: "%s"', $repo));
430+
}
431+
}
432+
433+
/**
434+
* Validate that a reference exists in a local repository.
435+
*
436+
* @param string $repo
437+
* The local repository path.
438+
* @param string $ref
439+
* The git reference to validate.
440+
*
441+
* @throws \RuntimeException
442+
* If the reference does not exist.
443+
*/
444+
protected function validateLocalRefExists(string $repo, string $ref): void {
445+
if (!$this->git instanceof Git) {
446+
$this->git = new Git($repo);
447+
}
448+
449+
try {
450+
// Use git rev-parse to check if the ref exists.
451+
$this->git->run('rev-parse', '--verify', $ref);
452+
}
453+
catch (\Exception $e) {
454+
throw new \RuntimeException(sprintf('Reference "%s" not found in local repository "%s"', $ref, $repo), $e->getCode(), $e);
455+
}
456+
}
457+
332458
}

.vortex/installer/tests/Unit/Downloader/RepositoryDownloaderTest.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ public function testArchiveFromLocalHandlesGitFailure(): void {
230230
exec('git commit -m "Initial commit" 2>&1');
231231
$downloader = new RepositoryDownloader();
232232
$this->expectException(\RuntimeException::class);
233-
$this->expectExceptionMessage('Failed to create archive from local repository');
233+
$this->expectExceptionMessage('Reference "nonexistent-ref" not found in local repository');
234234
$downloader->download($temp_repo_dir, 'nonexistent-ref', $temp_dest_dir);
235235
}
236236

@@ -245,8 +245,8 @@ public function testDiscoverLatestReleaseRemoteWithGithubToken(): void {
245245
$mock_response->method('getBody')->willReturn($mock_body);
246246
$mock_body->method('getContents')->willReturn($release_json);
247247
$mock_response->method('getStatusCode')->willReturn(200);
248-
// Only the API call uses httpClient now.
249-
$mock_http_client->expects($this->once())->method('request')->willReturnCallback(function ($method, $url, array|\ArrayAccess $options) use ($mock_response): ResponseInterface {
248+
// Two calls: HEAD for repo validation, GET for releases API.
249+
$mock_http_client->expects($this->exactly(2))->method('request')->willReturnCallback(function ($method, $url, array|\ArrayAccess $options) use ($mock_response): ResponseInterface {
250250
$this->assertArrayHasKey('headers', $options);
251251
$this->assertArrayHasKey('Authorization', $options['headers']);
252252
$this->assertEquals('Bearer test_token_12345', $options['headers']['Authorization']);
@@ -826,7 +826,7 @@ public static function providerDiscoverLatestReleaseRemote(): array {
826826
'skipMockSetup' => FALSE,
827827
'expectedVersion' => NULL,
828828
'expectedException' => \RuntimeException::class,
829-
'expectedMessage' => 'Unable to download release information from',
829+
'expectedMessage' => 'Unable to access repository',
830830
],
831831
'empty response' => [
832832
'repo' => 'https://github.com/user/repo',

0 commit comments

Comments
 (0)