diff --git a/src/Console/Command/PurgeCommand.php b/src/Console/Command/PurgeCommand.php index bbef3d2..548c46a 100644 --- a/src/Console/Command/PurgeCommand.php +++ b/src/Console/Command/PurgeCommand.php @@ -77,7 +77,7 @@ class PurgeCommand extends BaseCommand } $packageSelection = new PackageSelection($output, $outputDir, $config, false); - $packages = $packageSelection->load(); + $packages = $packageSelection->load(true); $satis_homepage = getenv('SATIS_HOMEPAGE'); $prefix = sprintf( diff --git a/src/PackageSelection/PackageSelection.php b/src/PackageSelection/PackageSelection.php index 552c425..ddadf76 100644 --- a/src/PackageSelection/PackageSelection.php +++ b/src/PackageSelection/PackageSelection.php @@ -293,7 +293,7 @@ class PackageSelection /** * @return PackageInterface[] */ - public function load(): array + public function load(bool $failOnMissingInclude = false): array { $packages = []; $rootJsonFile = new JsonFile($this->filename); @@ -330,11 +330,17 @@ class PackageSelection $includedJsonFile = new JsonFile($dirname . '/' . $file); if (!$includedJsonFile->exists()) { - $this->output->writeln(sprintf( - 'File \'%s\' does not exist, defined in "includes" in \'%s\'', + $message = sprintf( + 'File \'%s\' does not exist, defined in "includes" in \'%s\'', $includedJsonFile->getPath(), $rootJsonFile->getPath() - )); + ); + + if ($failOnMissingInclude) { + throw new \RuntimeException($message); + } + + $this->output->writeln('' . $message . ''); continue; } diff --git a/tests/Console/Command/PurgeCommandTest.php b/tests/Console/Command/PurgeCommandTest.php index 7894b6f..cf4e35c 100644 --- a/tests/Console/Command/PurgeCommandTest.php +++ b/tests/Console/Command/PurgeCommandTest.php @@ -534,6 +534,55 @@ class PurgeCommandTest extends TestCase self::assertStringContainsString('Removed empty directory', $tester->getDisplay()); } + /** + * A concurrent build can rewrite packages.json and drop the include it points to; + * the archives listed in that include are still needed and must not be purged. + */ + #[TestDox('execute() aborts without removing archives when an included file is missing')] + public function testExecuteAbortsWhenIncludedFileIsMissing(): void + { + $outputDir = vfsStream::url('satis/output-missing-include'); + mkdir($outputDir); + $archiveDir = $outputDir . '/dist'; + mkdir($archiveDir . '/vendor', 0755, true); + + $archiveFile = $archiveDir . '/vendor/package.tar.gz'; + file_put_contents($archiveFile, 'a'); + + $includeFile = 'include/all$missing.json'; + $this->writeOutputRepository($outputDir, $this->packageEntry( + 'vendor/package', + '1.0.0', + 'https://example.com/dist/vendor/package.tar.gz' + ), $includeFile); + unlink($outputDir . '/' . $includeFile); + + $this->writeSatisConfig([ + 'name' => 'test/satis-repo', + 'homepage' => 'https://example.com', + 'output-dir' => $outputDir, + 'archive' => [ + 'directory' => 'dist', + 'prefix-url' => 'https://example.com', + ], + ]); + + $tester = $this->createTester(); + $exception = null; + + try { + $tester->execute([ + 'file' => $this->configPath, + 'output-dir' => $outputDir, + ]); + } catch (\RuntimeException $exception) { + } + + self::assertInstanceOf(\RuntimeException::class, $exception); + self::assertStringContainsString($includeFile . '\' does not exist', $exception->getMessage()); + self::assertFileExists($archiveFile); + } + // --- Dry-run --- #[TestDox('execute() does not remove archives or empty dirs when dry-run is enabled')]