Skip unreferenced archives that are already gone when purging
This commit is contained in:
1 parent
222a1d882b
commit
a0d660f831
2 files changed
+134
-2
No files matched your search
@@ -16,6 +16,7 @@ namespace Composer\Satis\Console\Command;
|
|||||||
use Composer\Command\BaseCommand;
|
use Composer\Command\BaseCommand;
|
||||||
use Composer\Json\JsonFile;
|
use Composer\Json\JsonFile;
|
||||||
use Composer\Satis\PackageSelection\PackageSelection;
|
use Composer\Satis\PackageSelection\PackageSelection;
|
||||||
|
use Composer\Util\Filesystem;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
@@ -127,9 +128,24 @@ class PurgeCommand extends BaseCommand
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$filesystem = new Filesystem();
|
||||||
foreach ($unreferenced as $currentFile) {
|
foreach ($unreferenced as $currentFile) {
|
||||||
if (!$dryRun) {
|
if (!$dryRun) {
|
||||||
unlink($currentFile->getPathname());
|
try {
|
||||||
|
$filesystem->unlink($currentFile->getPathname());
|
||||||
|
} catch (\RuntimeException $exception) {
|
||||||
|
// Another process, e.g. a concurrent build or purge, may have removed the archive already.
|
||||||
|
if (file_exists($currentFile->getPathname())) {
|
||||||
|
throw $exception;
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'<warning>Skipped missing archive</warning>: <comment>%s</comment>',
|
||||||
|
$currentFile->getRelativePathname()
|
||||||
|
));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$output->writeln(sprintf(
|
$output->writeln(sprintf(
|
||||||
@@ -166,7 +182,7 @@ class PurgeCommand extends BaseCommand
|
|||||||
if (is_dir($path)
|
if (is_dir($path)
|
||||||
&& $depth > 0
|
&& $depth > 0
|
||||||
&& $this->removeEmptyDirectories($output, $path, $depth - 1)
|
&& $this->removeEmptyDirectories($output, $path, $depth - 1)
|
||||||
&& rmdir($path)
|
&& @rmdir($path)
|
||||||
) {
|
) {
|
||||||
$output->writeln(sprintf('<info>Removed empty directory</info>: <comment>%s</comment>', $path));
|
$output->writeln(sprintf('<info>Removed empty directory</info>: <comment>%s</comment>', $path));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use PHPUnit\Framework\Attributes\CoversClass;
|
|||||||
use PHPUnit\Framework\Attributes\DataProvider;
|
use PHPUnit\Framework\Attributes\DataProvider;
|
||||||
use PHPUnit\Framework\Attributes\TestDox;
|
use PHPUnit\Framework\Attributes\TestDox;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
use Symfony\Component\Console\Output\BufferedOutput;
|
use Symfony\Component\Console\Output\BufferedOutput;
|
||||||
use Symfony\Component\Console\Output\NullOutput;
|
use Symfony\Component\Console\Output\NullOutput;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
@@ -534,6 +535,121 @@ class PurgeCommandTest extends TestCase
|
|||||||
self::assertStringContainsString('Removed empty directory', $tester->getDisplay());
|
self::assertStringContainsString('Removed empty directory', $tester->getDisplay());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[TestDox('execute() skips unreferenced archives that are already gone when they are about to be removed')]
|
||||||
|
public function testExecuteSkipsArchivesThatDisappearDuringPurge(): void
|
||||||
|
{
|
||||||
|
$outputDir = vfsStream::url('satis/output-vanished');
|
||||||
|
mkdir($outputDir);
|
||||||
|
$archiveDir = $outputDir . '/dist';
|
||||||
|
mkdir($archiveDir . '/vendor', 0755, true);
|
||||||
|
|
||||||
|
$archives = [
|
||||||
|
$archiveDir . '/vendor/first.tar.gz',
|
||||||
|
$archiveDir . '/vendor/second.tar.gz',
|
||||||
|
];
|
||||||
|
foreach ($archives as $archive) {
|
||||||
|
file_put_contents($archive, 'x');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->writeOutputRepository($outputDir, $this->packageEntry(
|
||||||
|
'vendor/other',
|
||||||
|
'1.0.0',
|
||||||
|
'https://example.com/dist/vendor/other.tar.gz'
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->writeSatisConfig([
|
||||||
|
'name' => 'test/satis-repo',
|
||||||
|
'homepage' => 'https://example.com',
|
||||||
|
'output-dir' => $outputDir,
|
||||||
|
'archive' => [
|
||||||
|
'directory' => 'dist',
|
||||||
|
'prefix-url' => 'https://example.com',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Simulates a concurrent build or purge: once the first archive is removed, the others vanish too.
|
||||||
|
$output = new class($archives) extends BufferedOutput {
|
||||||
|
/**
|
||||||
|
* @param list<string> $archives
|
||||||
|
*/
|
||||||
|
public function __construct(private readonly array $archives)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doWrite(string $message, bool $newline): void
|
||||||
|
{
|
||||||
|
parent::doWrite($message, $newline);
|
||||||
|
|
||||||
|
if (str_contains($message, 'Removed archive')) {
|
||||||
|
foreach ($this->archives as $archive) {
|
||||||
|
if (file_exists($archive)) {
|
||||||
|
unlink($archive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$input = new ArrayInput([
|
||||||
|
'file' => $this->configPath,
|
||||||
|
'output-dir' => $outputDir,
|
||||||
|
]);
|
||||||
|
$input->setInteractive(false);
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$app->addCommand(new PurgeCommand());
|
||||||
|
|
||||||
|
$statusCode = $app->find('purge')->run($input, $output);
|
||||||
|
|
||||||
|
$display = $output->fetch();
|
||||||
|
self::assertSame(0, $statusCode);
|
||||||
|
self::assertSame(1, substr_count($display, 'Removed archive'));
|
||||||
|
self::assertStringContainsString('Skipped missing archive', $display);
|
||||||
|
self::assertStringContainsString('Done.', $display);
|
||||||
|
foreach ($archives as $archive) {
|
||||||
|
self::assertFileDoesNotExist($archive);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[TestDox('execute() throws when an unreferenced archive exists but cannot be removed')]
|
||||||
|
public function testExecuteThrowsWhenArchiveCannotBeRemoved(): void
|
||||||
|
{
|
||||||
|
$outputDir = vfsStream::url('satis/output-locked');
|
||||||
|
mkdir($outputDir);
|
||||||
|
$archiveDir = $outputDir . '/dist';
|
||||||
|
mkdir($archiveDir . '/vendor', 0755, true);
|
||||||
|
|
||||||
|
$unreferencedFile = $archiveDir . '/vendor/unreferenced.tar.gz';
|
||||||
|
file_put_contents($unreferencedFile, 'x');
|
||||||
|
chmod($archiveDir . '/vendor', 0555);
|
||||||
|
|
||||||
|
$this->writeOutputRepository($outputDir, $this->packageEntry(
|
||||||
|
'vendor/other',
|
||||||
|
'1.0.0',
|
||||||
|
'https://example.com/dist/vendor/other.tar.gz'
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->writeSatisConfig([
|
||||||
|
'name' => 'test/satis-repo',
|
||||||
|
'homepage' => 'https://example.com',
|
||||||
|
'output-dir' => $outputDir,
|
||||||
|
'archive' => [
|
||||||
|
'directory' => 'dist',
|
||||||
|
'prefix-url' => 'https://example.com',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tester = $this->createTester();
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('Could not delete ' . $unreferencedFile);
|
||||||
|
$tester->execute([
|
||||||
|
'file' => $this->configPath,
|
||||||
|
'output-dir' => $outputDir,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Dry-run ---
|
// --- Dry-run ---
|
||||||
|
|
||||||
#[TestDox('execute() does not remove archives or empty dirs when dry-run is enabled')]
|
#[TestDox('execute() does not remove archives or empty dirs when dry-run is enabled')]
|
||||||
|
|||||||
Reference in new issue
Block a user