From 0d20bae2ef3ff7cd2e24a4fcb8a1ca0b3f35788a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 17 Jan 2025 09:59:05 +0000 Subject: [PATCH 01/15] Added PieJsonEditor component --- .../ComposerIntegrationHandler.php | 14 ++--- .../PieComposerFactory.php | 8 +-- src/ComposerIntegration/PieJsonEditor.php | 60 ++++++++++++++++++ .../ComposerIntegration/PieJsonEditorTest.php | 63 +++++++++++++++++++ 4 files changed, 130 insertions(+), 15 deletions(-) create mode 100644 src/ComposerIntegration/PieJsonEditor.php create mode 100644 test/unit/ComposerIntegration/PieJsonEditorTest.php diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index d246a496..4a03f44e 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -7,7 +7,6 @@ use Composer\Composer; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; -use Composer\Json\JsonManipulator; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform; @@ -15,8 +14,6 @@ use Psr\Container\ContainerInterface; use function file_exists; -use function file_get_contents; -use function file_put_contents; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class ComposerIntegrationHandler @@ -47,10 +44,11 @@ public function __invoke( // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); - $originalPieJsonContent = file_get_contents($pieComposerJson); - $manipulator = new JsonManipulator($originalPieJsonContent); - $manipulator->addLink('require', $requestedPackageAndVersion->package, $recommendedRequireVersion, true); - file_put_contents($pieComposerJson, $manipulator->getContents()); + $pieJsonEditor = new PieJsonEditor($pieComposerJson); + $originalPieJsonContent = $pieJsonEditor->addRequire( + $requestedPackageAndVersion->package, + $recommendedRequireVersion !== '' ? $recommendedRequireVersion : '*', + ); // Refresh the Composer instance so it re-reads the updated pie.json $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); @@ -83,7 +81,7 @@ public function __invoke( if ($resultCode !== Installer::ERROR_NONE) { // Revert composer.json change - file_put_contents($pieComposerJson, $originalPieJsonContent); + $pieJsonEditor->revert($originalPieJsonContent); throw ComposerRunFailed::fromExitCode($resultCode); } diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index d135e611..3b419852 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -17,7 +17,6 @@ use Webmozart\Assert\Assert; use function file_exists; -use function file_put_contents; use function mkdir; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -60,12 +59,7 @@ public static function createPieComposer( $pieComposer = Platform::getPieJsonFilename($composerRequest->targetPlatform); - if (! file_exists($pieComposer)) { - file_put_contents( - $pieComposer, - "{\n}\n", - ); - } + (new PieJsonEditor($pieComposer))->ensureExists(); $io = $container->get(QuieterConsoleIO::class); $composer = (new PieComposerFactory($container, $composerRequest))->createComposer( diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php new file mode 100644 index 00000000..dc7cf6e4 --- /dev/null +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -0,0 +1,60 @@ +pieJsonFilename)) { + return; + } + + file_put_contents( + $this->pieJsonFilename, + "{\n}\n", + ); + } + + /** + * Add a package to the `require` section of the given `pie.json`. Returns + * the original `pie.json` content, in case it needs to be restored later. + * + * @param non-empty-string $package + * @param non-empty-string $version + */ + public function addRequire(string $package, string $version): string + { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + $manipulator = new JsonManipulator($originalPieJsonContent); + $manipulator->addLink('require', $package, $version, true); + file_put_contents($this->pieJsonFilename, $manipulator->getContents()); + + return $originalPieJsonContent; + } + + public function revert(string $originalPieJsonContent): void + { + file_put_contents($this->pieJsonFilename, $originalPieJsonContent); + } +} diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php new file mode 100644 index 00000000..5fbccfe1 --- /dev/null +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -0,0 +1,63 @@ +ensureExists(); + + self::assertFileExists($testPieJson); + self::assertSame("{\n}\n", file_get_contents($testPieJson)); + } + + public function testCanAddRequire(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson); + $editor->ensureExists(); + + $editor->addRequire('foo/bar', '^1.2'); + self::assertSame( + <<<'EOF' +{ + "require": { + "foo/bar": "^1.2" + } +} +EOF, + trim(file_get_contents($testPieJson)), + ); + } + + public function testCanRevert(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson); + $editor->ensureExists(); + $originalContent = $editor->addRequire('foo/bar', '^1.2'); + $editor->revert($originalContent); + self::assertSame($originalContent, file_get_contents($testPieJson)); + } +} From 26aa1ff72b06749b19d55d37ec17f4ebe0589ee6 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 17 Jan 2025 10:16:45 +0000 Subject: [PATCH 02/15] Added PieJsonEditor->addRepository method --- src/ComposerIntegration/PieJsonEditor.php | 39 ++++++++++++++--- .../ComposerIntegration/PieJsonEditorTest.php | 42 ++++++++++++++++--- 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index dc7cf6e4..edbc739a 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -6,8 +6,6 @@ use Composer\Config\JsonConfigSource; use Composer\Json\JsonFile; -use Composer\Json\JsonManipulator; -use Php\Pie\Platform\TargetPlatform; use function file_exists; use function file_get_contents; @@ -46,9 +44,12 @@ public function ensureExists(): void public function addRequire(string $package, string $version): string { $originalPieJsonContent = file_get_contents($this->pieJsonFilename); - $manipulator = new JsonManipulator($originalPieJsonContent); - $manipulator->addLink('require', $package, $version, true); - file_put_contents($this->pieJsonFilename, $manipulator->getContents()); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + ))->addLink('require', $package, $version); return $originalPieJsonContent; } @@ -57,4 +58,32 @@ public function revert(string $originalPieJsonContent): void { file_put_contents($this->pieJsonFilename, $originalPieJsonContent); } + + /** + * Add a repository to the given `pie.json`. Returns the original + * `pie.json` content, in case it needs to be restored later. + * + * @param non-empty-string $name + * @param 'vcs'|'path' $type + * @param non-empty-string $url + */ + public function addRepository( + string $name, + string $type, + string $url, + ): string { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + )) + ->addRepository($name, [ + 'type' => $type, + 'url' => $url, + ]); + + return $originalPieJsonContent; + } } diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php index 5fbccfe1..ddf74b6f 100644 --- a/test/unit/ComposerIntegration/PieJsonEditorTest.php +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -40,12 +40,12 @@ public function testCanAddRequire(): void $editor->addRequire('foo/bar', '^1.2'); self::assertSame( <<<'EOF' -{ - "require": { - "foo/bar": "^1.2" - } -} -EOF, + { + "require": { + "foo/bar": "^1.2" + } + } + EOF, trim(file_get_contents($testPieJson)), ); } @@ -60,4 +60,34 @@ public function testCanRevert(): void $editor->revert($originalContent); self::assertSame($originalContent, file_get_contents($testPieJson)); } + + public function testCanAddRepostiory(): void + { + $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; + + $editor = new PieJsonEditor($testPieJson); + $editor->ensureExists(); + + $originalContent = $editor->addRepository( + 'myrepo', + 'vcs', + 'https://github.com/php/pie', + ); + + self::assertSame("{\n}\n", $originalContent); + + self::assertSame( + <<<'EOF' + { + "repositories": { + "myrepo": { + "type": "vcs", + "url": "https://github.com/php/pie" + } + } + } + EOF, + trim(file_get_contents($testPieJson)), + ); + } } From 37c2e3edef21a983f92a0e5016bef09925439102 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 21 Jan 2025 08:20:40 +0000 Subject: [PATCH 03/15] Added PieJsonEditor removeRepository method --- src/ComposerIntegration/PieJsonEditor.php | 25 ++++++++++++++++--- .../ComposerIntegration/PieJsonEditorTest.php | 21 ++++++++++++---- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index edbc739a..52a7e391 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -63,12 +63,10 @@ public function revert(string $originalPieJsonContent): void * Add a repository to the given `pie.json`. Returns the original * `pie.json` content, in case it needs to be restored later. * - * @param non-empty-string $name * @param 'vcs'|'path' $type * @param non-empty-string $url */ public function addRepository( - string $name, string $type, string $url, ): string { @@ -79,11 +77,32 @@ public function addRepository( $this->pieJsonFilename, ), )) - ->addRepository($name, [ + ->addRepository($url, [ 'type' => $type, 'url' => $url, ]); return $originalPieJsonContent; } + + /** + * Remove a repository from the given `pie.json`. Returns the original + * `pie.json` content, in case it needs to be restored later. + * + * @param non-empty-string $name + */ + public function removeRepository( + string $name, + ): string { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + )) + ->removeRepository($name); + + return $originalPieJsonContent; + } } diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php index ddf74b6f..094b9f08 100644 --- a/test/unit/ComposerIntegration/PieJsonEditorTest.php +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -61,7 +61,7 @@ public function testCanRevert(): void self::assertSame($originalContent, file_get_contents($testPieJson)); } - public function testCanAddRepostiory(): void + public function testCanAddAndRemoveRepositories(): void { $testPieJson = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_json_test_', true) . '.json'; @@ -69,23 +69,34 @@ public function testCanAddRepostiory(): void $editor->ensureExists(); $originalContent = $editor->addRepository( - 'myrepo', 'vcs', 'https://github.com/php/pie', ); self::assertSame("{\n}\n", $originalContent); - self::assertSame( - <<<'EOF' + $expectedRepoContent = <<<'EOF' { "repositories": { - "myrepo": { + "https://github.com/php/pie": { "type": "vcs", "url": "https://github.com/php/pie" } } } + EOF; + + self::assertSame($expectedRepoContent, trim(file_get_contents($testPieJson))); + + $originalContent2 = $editor->removeRepository('https://github.com/php/pie'); + self::assertSame($expectedRepoContent, trim($originalContent2)); + + self::assertSame( + <<<'EOF' + { + "repositories": { + } + } EOF, trim(file_get_contents($testPieJson)), ); From cb1069dc870cb0718ce527373c7dc37c160b90b6 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 21 Jan 2025 09:55:25 +0000 Subject: [PATCH 04/15] Added command helper to write out Composer repositories in use --- src/Command/CommandHelper.php | 35 +++++++++++++++++++ test/unit/Command/CommandHelperTest.php | 46 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 6fc1368f..59680f6c 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -4,7 +4,11 @@ namespace Php\Pie\Command; +use Composer\Composer; use Composer\Package\Version\VersionParser; +use Composer\Repository\ComposerRepository; +use Composer\Repository\PathRepository; +use Composer\Repository\VcsRepository; use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\DependencyResolver\Package; @@ -263,4 +267,35 @@ public static function processConfigureOptionsFromInput(Package $package, InputI return $configureOptionsValues; } + + public static function listRepositories(Composer $composer, OutputInterface $output): void + { + $output->writeln('The following repositories are in use for this Target PHP:'); + + foreach ($composer->getRepositoryManager()->getRepositories() as $repo) { + if ($repo instanceof ComposerRepository) { + $output->writeln(' - Packagist (cannot be removed)'); + continue; + } + + if ($repo instanceof VcsRepository) { + /** @psalm-suppress InternalMethod */ + $output->writeln(sprintf( + ' - VCS Repository (%s)', + $repo->getDriver()?->getUrl() ?? 'no url?', + )); + continue; + } + + if (! $repo instanceof PathRepository) { + continue; + } + + $repoConfig = $repo->getRepoConfig(); + $output->writeln(sprintf( + ' - Path Repository (%s)', + array_key_exists('url', $repoConfig) && is_string($repoConfig['url']) && $repoConfig['url'] !== '' ? $repoConfig['url'] : 'no path?', + )); + } + } } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 34183746..b41da7a8 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -4,7 +4,13 @@ namespace Php\PieUnitTest\Command; +use Composer\Composer; use Composer\Package\CompletePackage; +use Composer\Repository\ComposerRepository; +use Composer\Repository\PathRepository; +use Composer\Repository\RepositoryManager; +use Composer\Repository\Vcs\GitHubDriver; +use Composer\Repository\VcsRepository; use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\Command\CommandHelper; @@ -22,10 +28,12 @@ use Symfony\Component\Console\Input\InputDefinition; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\NullOutput; use function array_combine; use function array_map; +use function trim; #[CoversClass(CommandHelper::class)] final class CommandHelperTest extends TestCase @@ -175,4 +183,42 @@ public function testWindowsMachinesCannotUseWithPhpizePathOption(): void $this->expectExceptionMessage('The --with-phpize-path=/path/to/phpize cannot be used on Windows.'); CommandHelper::determineTargetPlatformFromInputs($input, $output); } + + public function testListRepositories(): void + { + $output = new BufferedOutput(); + + $composerRepo = $this->createMock(ComposerRepository::class); + + $githubRepoDriver = $this->createMock(GitHubDriver::class); + $githubRepoDriver->method('getUrl')->willReturn('https://github.com/php/pie'); + + $vcsRepo = $this->createMock(VcsRepository::class); + $vcsRepo->method('getDriver')->willReturn($githubRepoDriver); + + $pathRepo = $this->createMock(PathRepository::class); + $pathRepo->method('getRepoConfig')->willReturn(['url' => '/path/to/repo']); + + $repoManager = $this->createMock(RepositoryManager::class); + $repoManager->method('getRepositories')->willReturn([ + $composerRepo, + $vcsRepo, + $pathRepo, + ]); + + $composer = $this->createMock(Composer::class); + $composer->method('getRepositoryManager')->willReturn($repoManager); + + CommandHelper::listRepositories($composer, $output); + + self::assertSame( + <<<'OUTPUT' + The following repositories are in use for this Target PHP: + - Packagist (cannot be removed) + - VCS Repository (https://github.com/php/pie) + - Path Repository (/path/to/repo) + OUTPUT, + trim($output->fetch()), + ); + } } From 2e67ee51ec91010a27d66c410003f619d1ac2b6f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 22 Jan 2025 08:50:43 +0000 Subject: [PATCH 05/15] Add repository:* commands to manage path/vcs repositories --- bin/pie | 6 + features/install-extensions.feature | 2 +- features/manage-repositories.feature | 11 ++ src/Command/RepositoryAddCommand.php | 89 +++++++++++++++ src/Command/RepositoryListCommand.php | 49 ++++++++ src/Command/RepositoryRemoveCommand.php | 70 ++++++++++++ src/Container.php | 6 + test/behaviour/CliContext.php | 39 +++++++ .../RepositoryManagementCommandsTest.php | 106 ++++++++++++++++++ 9 files changed, 377 insertions(+), 1 deletion(-) create mode 100644 features/manage-repositories.feature create mode 100644 src/Command/RepositoryAddCommand.php create mode 100644 src/Command/RepositoryListCommand.php create mode 100644 src/Command/RepositoryRemoveCommand.php create mode 100644 test/integration/Command/RepositoryManagementCommandsTest.php diff --git a/bin/pie b/bin/pie index 88443670..2b907bb1 100755 --- a/bin/pie +++ b/bin/pie @@ -9,6 +9,9 @@ use Php\Pie\Command\BuildCommand; use Php\Pie\Command\DownloadCommand; use Php\Pie\Command\InfoCommand; use Php\Pie\Command\InstallCommand; +use Php\Pie\Command\RepositoryAddCommand; +use Php\Pie\Command\RepositoryListCommand; +use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; use Php\Pie\Util\PieVersion; use Symfony\Component\Console\Application; @@ -31,6 +34,9 @@ $application->setCommandLoader(new ContainerCommandLoader( 'install' => InstallCommand::class, 'info' => InfoCommand::class, 'show' => ShowCommand::class, + 'repository:list' => RepositoryListCommand::class, + 'repository:add' => RepositoryAddCommand::class, + 'repository:remove' => RepositoryRemoveCommand::class, ] )); diff --git a/features/install-extensions.feature b/features/install-extensions.feature index 27841d29..20422ed7 100644 --- a/features/install-extensions.feature +++ b/features/install-extensions.feature @@ -1,4 +1,4 @@ -Feature: Extensions can be installed with Behat +Feature: Extensions can be installed with PIE Example: The latest version of an extension can be downloaded When I run a command to download the latest version of an extension diff --git a/features/manage-repositories.feature b/features/manage-repositories.feature new file mode 100644 index 00000000..6fd69b50 --- /dev/null +++ b/features/manage-repositories.feature @@ -0,0 +1,11 @@ +Feature: Package repositories can be managed with PIE + + Example: A package repository can be added + Given no repositories have previously been added + When I add a package repository + Then I should see the package repository can be used by PIE + + Example: A package repository can be removed + Given I have previously added a package repository + When I remove the package repository + Then I should see the package repository is not used by PIE diff --git a/src/Command/RepositoryAddCommand.php b/src/Command/RepositoryAddCommand.php new file mode 100644 index 00000000..307d92a1 --- /dev/null +++ b/src/Command/RepositoryAddCommand.php @@ -0,0 +1,89 @@ +addArgument( + self::ARG_TYPE, + InputArgument::REQUIRED, + 'Specify the type of the repository, e.g. vcs, path', + ); + $this->addArgument( + self::ARG_URL, + InputArgument::REQUIRED, + 'Specify the URL of the repository, e.g. a Github/Gitlab URL, or a filesystem path', + ); + $this->addUsage('lol'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $pieJsonFilename = Platform::getPieJsonFilename($targetPlatform); + + $type = (string) $input->getArgument(self::ARG_TYPE); + /** @psalm-var 'vcs'|'path' $type */ + Assert::inArray($type, self::ALLOWED_TYPES); + + $url = $originalUrl = (string) $input->getArgument(self::ARG_URL); + + if ($type === 'path') { + $url = realpath($originalUrl); + } + + Assert::stringNotEmpty($url, 'Could not resolve ' . $originalUrl . ' to a real path'); + + (new PieJsonEditor($pieJsonFilename))->addRepository($type, $url); + + CommandHelper::listRepositories( + PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + $output, + CommandHelper::determineTargetPlatformFromInputs($input, $output), + ), + ), + $output, + ); + + return 0; + } +} diff --git a/src/Command/RepositoryListCommand.php b/src/Command/RepositoryListCommand.php new file mode 100644 index 00000000..a2d6dddd --- /dev/null +++ b/src/Command/RepositoryListCommand.php @@ -0,0 +1,49 @@ +container, + PieComposerRequest::noOperation( + $output, + CommandHelper::determineTargetPlatformFromInputs($input, $output), + ), + ), + $output, + ); + + return 0; + } +} diff --git a/src/Command/RepositoryRemoveCommand.php b/src/Command/RepositoryRemoveCommand.php new file mode 100644 index 00000000..bd9966fb --- /dev/null +++ b/src/Command/RepositoryRemoveCommand.php @@ -0,0 +1,70 @@ +addArgument( + self::ARG_URL, + InputArgument::REQUIRED, + 'Specify the URL of the repository, e.g. a Github/Gitlab URL, or a filesystem path', + ); + $this->addUsage('lol'); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $pieJsonFilename = Platform::getPieJsonFilename($targetPlatform); + + $url = (string) $input->getArgument(self::ARG_URL); + Assert::stringNotEmpty($url); + + (new PieJsonEditor($pieJsonFilename))->removeRepository($url); + + CommandHelper::listRepositories( + PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + $output, + CommandHelper::determineTargetPlatformFromInputs($input, $output), + ), + ), + $output, + ); + + return 0; + } +} diff --git a/src/Container.php b/src/Container.php index 28e1f885..6f8ef418 100644 --- a/src/Container.php +++ b/src/Container.php @@ -13,6 +13,9 @@ use Php\Pie\Command\DownloadCommand; use Php\Pie\Command\InfoCommand; use Php\Pie\Command\InstallCommand; +use Php\Pie\Command\RepositoryAddCommand; +use Php\Pie\Command\RepositoryListCommand; +use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; use Php\Pie\ComposerIntegration\MinimalHelperSet; use Php\Pie\ComposerIntegration\QuieterConsoleIO; @@ -46,6 +49,9 @@ public static function factory(): ContainerInterface $container->singleton(InstallCommand::class); $container->singleton(InfoCommand::class); $container->singleton(ShowCommand::class); + $container->singleton(RepositoryListCommand::class); + $container->singleton(RepositoryAddCommand::class); + $container->singleton(RepositoryRemoveCommand::class); $container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO { return new QuieterConsoleIO( diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 801dbe9b..31b2f9a5 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -146,4 +146,43 @@ public function iHaveAnInvalidExtensionInstalled(): void { $this->phpArguments = ['-d', 'extension=invalid_extension']; } + + #[When('I add a package repository')] + public function iAddAPackageRepository(): void + { + $this->runPieCommand(['repository:add', 'path', __DIR__]); + } + + #[Then('I should see the package repository can be used by PIE')] + public function iShouldSeeThePackageRepositoryCanBeUsedByPie(): void + { + Assert::notNull($this->output); + Assert::contains($this->output, 'Path Repository (' . __DIR__ . ')'); + } + + #[Given('I have previously added a package repository')] + public function iHavePreviouslyAddedAPackageRepository(): void + { + $this->noRepositoriesHavePreviouslyBeenAdded(); + $this->iAddAPackageRepository(); + } + + #[Given('no repositories have previously been added')] + public function noRepositoriesHavePreviouslyBeenAdded(): void + { + $this->iRemoveThePackageRepository(); + } + + #[When('I remove the package repository')] + public function iRemoveThePackageRepository(): void + { + $this->runPieCommand(['repository:remove', __DIR__]); + } + + #[Then('I should see the package repository is not used by PIE')] + public function iShouldSeeThePackageRepositoryIsNotUsedByPie(): void + { + Assert::notNull($this->output); + Assert::notContains($this->output, 'Path repository (' . __DIR__ . ')'); + } } diff --git a/test/integration/Command/RepositoryManagementCommandsTest.php b/test/integration/Command/RepositoryManagementCommandsTest.php new file mode 100644 index 00000000..c9e223ff --- /dev/null +++ b/test/integration/Command/RepositoryManagementCommandsTest.php @@ -0,0 +1,106 @@ +listCommand = new CommandTester(Container::factory()->get(RepositoryListCommand::class)); + $this->addCommand = new CommandTester(Container::factory()->get(RepositoryAddCommand::class)); + $this->removeCommand = new CommandTester(Container::factory()->get(RepositoryRemoveCommand::class)); + + $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL]); + $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL]); + $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL . '.git']); + } + + public function testPathRepositoriesCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + + $this->addCommand->execute([ + 'type' => 'path', + 'url' => self::EXAMPLE_PATH_REPOSITORY_URL, + ]); + + $this->assertRepositoryListDisplayed( + [ + 'Path Repository (' . self::EXAMPLE_PATH_REPOSITORY_URL . ')', + 'Packagist (cannot be removed)', + ], + ); + + $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL]); + $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + } + + public function testVcsRepositoriesCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + + $this->addCommand->execute([ + 'type' => 'vcs', + 'url' => self::EXAMPLE_VCS_REPOSITORY_URL, + ]); + + $this->assertRepositoryListDisplayed( + [ + 'VCS Repository (' . self::EXAMPLE_VCS_REPOSITORY_URL . '.git)', + 'Packagist (cannot be removed)', + ], + ); + + $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL]); + $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + } + + /** @param list $expectedRepositories */ + private function assertRepositoryListDisplayed(array $expectedRepositories): void + { + $this->listCommand->execute([]); + $this->listCommand->assertCommandIsSuccessful(); + + $outputString = $this->listCommand->getDisplay(); + + self::assertEquals( + $expectedRepositories, + array_values(array_map( + static fn ($line) => substr($line, 4), + array_filter( + explode(PHP_EOL, $outputString), + static fn ($line): bool => str_starts_with($line, ' - '), + ), + )), + ); + } +} From bb8a463997376891b3561074e554b5255b6478b1 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 22 Jan 2025 10:49:26 +0000 Subject: [PATCH 06/15] Add support for non-Packagist Composer repos in the back end of the PieJsonEditor NOTE: that this does not enable support in the repository add command, as Private Packagist does not yet appear to pass down the `php-ext` metadata, meaning some packages won't actually work. See: https://github.com/php/pie/issues/175 --- src/Command/CommandHelper.php | 11 ++++++++++- src/ComposerIntegration/PieJsonEditor.php | 4 ++-- .../Command/RepositoryManagementCommandsTest.php | 12 ++++++------ test/unit/Command/CommandHelperTest.php | 12 +++++++++--- 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 59680f6c..1fc4c7a9 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -274,7 +274,16 @@ public static function listRepositories(Composer $composer, OutputInterface $out foreach ($composer->getRepositoryManager()->getRepositories() as $repo) { if ($repo instanceof ComposerRepository) { - $output->writeln(' - Packagist (cannot be removed)'); + $repoConfig = $repo->getRepoConfig(); + + $repoUrl = array_key_exists('url', $repoConfig) && is_string($repoConfig['url']) && $repoConfig['url'] !== '' ? $repoConfig['url'] : null; + + if ($repoUrl === 'https://repo.packagist.org') { + $output->writeln(' - Packagist'); + continue; + } + + $output->writeln(sprintf(' - Composer (%s)', $repoUrl ?? 'no url?')); continue; } diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index 52a7e391..d7f516ae 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -63,8 +63,8 @@ public function revert(string $originalPieJsonContent): void * Add a repository to the given `pie.json`. Returns the original * `pie.json` content, in case it needs to be restored later. * - * @param 'vcs'|'path' $type - * @param non-empty-string $url + * @param 'vcs'|'path'|'composer' $type + * @param non-empty-string $url */ public function addRepository( string $type, diff --git a/test/integration/Command/RepositoryManagementCommandsTest.php b/test/integration/Command/RepositoryManagementCommandsTest.php index c9e223ff..de9de3ec 100644 --- a/test/integration/Command/RepositoryManagementCommandsTest.php +++ b/test/integration/Command/RepositoryManagementCommandsTest.php @@ -46,7 +46,7 @@ public function setUp(): void public function testPathRepositoriesCanBeManaged(): void { - $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + $this->assertRepositoryListDisplayed(['Packagist']); $this->addCommand->execute([ 'type' => 'path', @@ -56,17 +56,17 @@ public function testPathRepositoriesCanBeManaged(): void $this->assertRepositoryListDisplayed( [ 'Path Repository (' . self::EXAMPLE_PATH_REPOSITORY_URL . ')', - 'Packagist (cannot be removed)', + 'Packagist', ], ); $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL]); - $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + $this->assertRepositoryListDisplayed(['Packagist']); } public function testVcsRepositoriesCanBeManaged(): void { - $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + $this->assertRepositoryListDisplayed(['Packagist']); $this->addCommand->execute([ 'type' => 'vcs', @@ -76,12 +76,12 @@ public function testVcsRepositoriesCanBeManaged(): void $this->assertRepositoryListDisplayed( [ 'VCS Repository (' . self::EXAMPLE_VCS_REPOSITORY_URL . '.git)', - 'Packagist (cannot be removed)', + 'Packagist', ], ); $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL]); - $this->assertRepositoryListDisplayed(['Packagist (cannot be removed)']); + $this->assertRepositoryListDisplayed(['Packagist']); } /** @param list $expectedRepositories */ diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index b41da7a8..fc22212f 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -188,7 +188,11 @@ public function testListRepositories(): void { $output = new BufferedOutput(); - $composerRepo = $this->createMock(ComposerRepository::class); + $packagistRepo = $this->createMock(ComposerRepository::class); + $packagistRepo->method('getRepoConfig')->willReturn(['url' => 'https://repo.packagist.org']); + + $privatePackagistRepo = $this->createMock(ComposerRepository::class); + $privatePackagistRepo->method('getRepoConfig')->willReturn(['url' => 'https://repo.packagist.com/example']); $githubRepoDriver = $this->createMock(GitHubDriver::class); $githubRepoDriver->method('getUrl')->willReturn('https://github.com/php/pie'); @@ -201,7 +205,8 @@ public function testListRepositories(): void $repoManager = $this->createMock(RepositoryManager::class); $repoManager->method('getRepositories')->willReturn([ - $composerRepo, + $packagistRepo, + $privatePackagistRepo, $vcsRepo, $pathRepo, ]); @@ -214,7 +219,8 @@ public function testListRepositories(): void self::assertSame( <<<'OUTPUT' The following repositories are in use for this Target PHP: - - Packagist (cannot be removed) + - Packagist + - Composer (https://repo.packagist.com/example) - VCS Repository (https://github.com/php/pie) - Path Repository (/path/to/repo) OUTPUT, From 1b30f410316f61af6b44714193bdd14e4c11ad75 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 23 Jan 2025 08:35:51 +0000 Subject: [PATCH 07/15] Normalise all the things for Windows --- src/ComposerIntegration/PieJsonEditor.php | 10 +++- test/unit/Command/CommandHelperTest.php | 7 +-- .../ComposerIntegration/PieJsonEditorTest.php | 49 +++++++++++++------ 3 files changed, 47 insertions(+), 19 deletions(-) diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index d7f516ae..a567b20b 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -10,6 +10,7 @@ use function file_exists; use function file_get_contents; use function file_put_contents; +use function str_replace; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class PieJsonEditor @@ -77,7 +78,7 @@ public function addRepository( $this->pieJsonFilename, ), )) - ->addRepository($url, [ + ->addRepository($this->normaliseRepositoryName($url), [ 'type' => $type, 'url' => $url, ]); @@ -101,8 +102,13 @@ public function removeRepository( $this->pieJsonFilename, ), )) - ->removeRepository($name); + ->removeRepository($this->normaliseRepositoryName($name)); return $originalPieJsonContent; } + + private function normaliseRepositoryName(string $url): string + { + return str_replace('\\', '/', $url); + } } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index fc22212f..f70eb932 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -33,6 +33,7 @@ use function array_combine; use function array_map; +use function str_replace; use function trim; #[CoversClass(CommandHelper::class)] @@ -217,14 +218,14 @@ public function testListRepositories(): void CommandHelper::listRepositories($composer, $output); self::assertSame( - <<<'OUTPUT' + str_replace("\r\n", "\n", <<<'OUTPUT' The following repositories are in use for this Target PHP: - Packagist - Composer (https://repo.packagist.com/example) - VCS Repository (https://github.com/php/pie) - Path Repository (/path/to/repo) - OUTPUT, - trim($output->fetch()), + OUTPUT), + str_replace("\r\n", "\n", trim($output->fetch())), ); } } diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php index 094b9f08..9ceb3f69 100644 --- a/test/unit/ComposerIntegration/PieJsonEditorTest.php +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -9,8 +9,9 @@ use PHPUnit\Framework\TestCase; use function file_get_contents; +use function json_decode; +use function json_encode; use function sys_get_temp_dir; -use function trim; use function uniqid; use const DIRECTORY_SEPARATOR; @@ -27,7 +28,10 @@ public function testCreatingPieJson(): void (new PieJsonEditor($testPieJson))->ensureExists(); self::assertFileExists($testPieJson); - self::assertSame("{\n}\n", file_get_contents($testPieJson)); + self::assertSame( + $this->normaliseJson("{\n}\n"), + $this->normaliseJson(file_get_contents($testPieJson)), + ); } public function testCanAddRequire(): void @@ -39,14 +43,14 @@ public function testCanAddRequire(): void $editor->addRequire('foo/bar', '^1.2'); self::assertSame( - <<<'EOF' + $this->normaliseJson(<<<'EOF' { "require": { "foo/bar": "^1.2" } } - EOF, - trim(file_get_contents($testPieJson)), + EOF), + $this->normaliseJson(file_get_contents($testPieJson)), ); } @@ -58,7 +62,10 @@ public function testCanRevert(): void $editor->ensureExists(); $originalContent = $editor->addRequire('foo/bar', '^1.2'); $editor->revert($originalContent); - self::assertSame($originalContent, file_get_contents($testPieJson)); + self::assertSame( + $this->normaliseJson($originalContent), + $this->normaliseJson(file_get_contents($testPieJson)), + ); } public function testCanAddAndRemoveRepositories(): void @@ -73,9 +80,12 @@ public function testCanAddAndRemoveRepositories(): void 'https://github.com/php/pie', ); - self::assertSame("{\n}\n", $originalContent); + self::assertSame( + $this->normaliseJson("{\n}\n"), + $this->normaliseJson($originalContent), + ); - $expectedRepoContent = <<<'EOF' + $expectedRepoContent = $this->normaliseJson(<<<'EOF' { "repositories": { "https://github.com/php/pie": { @@ -84,21 +94,32 @@ public function testCanAddAndRemoveRepositories(): void } } } - EOF; + EOF); - self::assertSame($expectedRepoContent, trim(file_get_contents($testPieJson))); + self::assertSame( + $expectedRepoContent, + $this->normaliseJson(file_get_contents($testPieJson)), + ); $originalContent2 = $editor->removeRepository('https://github.com/php/pie'); - self::assertSame($expectedRepoContent, trim($originalContent2)); + self::assertSame( + $expectedRepoContent, + $this->normaliseJson($originalContent2), + ); self::assertSame( - <<<'EOF' + $this->normaliseJson(<<<'EOF' { "repositories": { } } - EOF, - trim(file_get_contents($testPieJson)), + EOF), + $this->normaliseJson(file_get_contents($testPieJson)), ); } + + private function normaliseJson(string $fileContent): string + { + return json_encode(json_decode($fileContent)); + } } From 6289eaf1b8490363c51e120b8eab3e6828c1e10c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 23 Jan 2025 10:37:20 +0000 Subject: [PATCH 08/15] Documentation for auto-INI and repositories --- docs/usage.md | 45 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index ac4e7179..209411f3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -37,7 +37,7 @@ COPY --from=ghcr.io/php/pie:bin /pie /usr/bin/pie Instead of `bin` tag (which represents latest binary-only image) you can also use explicit version (in `x.y.z-bin` format). Use [GitHub registry](https://ghcr.io/php/pie) to find available tags. -> [!IMPORTANT] +> [!IMPORTANT] > Binary-only images don't include PHP runtime so you can't use them for _running_ PIE. This is just an alternative way of distributing PHAR file, you still need to satisfy PIE's runtime requirements on your own. #### Example of PIE working in a Dockerfile @@ -215,6 +215,43 @@ pie install example/some-extension --with-some-library-name=/path/to/the/lib --e ### Configuring the INI file -At the moment, PIE does not configure the INI file, although this improvement -is planned soon. In the meantime, you must enable the extension after installing -by adding a line such as `extension=foo` to your `php.ini`. +PIE will automatically try to enable the extension by adding `extension=...` or +`zend_extension=...` in the appropriate INI file. If you want to disable this +behaviour, pass the `--skip-enable-extension` flag to your `pie install` +command. The following techniques are used to attempt to enable the extension: + + * `phpenmod`, if using the deb.sury.org distribution + * `docker-php-ext-enable` if using Docker's PHP image + * Add a new file to the "additional .ini file" path, if configured + * Append to the standard php.ini, if configured + +If none of these techniques work, or you used the `--skip-enable-extension` +flag, PIE will warn you that the extension was not enabled, and will note that +you must enable the extension yourself. + +### Adding non-Packagist.org repositories + +Sometimes you may want to install an extension from a package repository other +than Packagist.org (such as [Private Packagist](https://packagist.com/)), or +from a local directory. Since PIE is based heavily on Composer, it is possible +to use some other repository types: + +* `pie repository:add [--with-php-config=...] path /path/to/your/local/extension` +* `pie repository:add [--with-php-config=...] vcs https://github.com/youruser/yourextension` +* `pie repository:add [--with-php-config=...] composer https://repo.packagist.com/your-private-packagist/` + +The `repository:*` commands all support the optional `--with-php-config` flag +to allow you to specify which PHP installation to use (for example, if you have +multiple PHP installations on one machine). The above added repositories can be +removed too, using the inverse `repository:remove` commands: + +* `pie repository:remove [--with-php-config=...] /path/to/your/local/extension` +* `pie repository:remove [--with-php-config=...] https://github.com/youruser/yourextension` +* `pie repository:remove [--with-php-config=...] https://repo.packagist.com/your-private-packagist/` + +Note you do not need to specify the repository type in `repository:remove`, +just the URL. + +You can list the repositories for the target PHP installation with: + +* `pie repository:list [--with-php-config=...]` From a1e3c7bc508f5cd847187867ca01cd9e07d1ee5c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 23 Jan 2025 10:56:15 +0000 Subject: [PATCH 09/15] Added ability to add a Composer repo --- src/Command/RepositoryAddCommand.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Command/RepositoryAddCommand.php b/src/Command/RepositoryAddCommand.php index 307d92a1..09852a34 100644 --- a/src/Command/RepositoryAddCommand.php +++ b/src/Command/RepositoryAddCommand.php @@ -27,7 +27,7 @@ final class RepositoryAddCommand extends Command private const ARG_TYPE = 'type'; private const ARG_URL = 'url'; - private const ALLOWED_TYPES = ['vcs', 'path']; + private const ALLOWED_TYPES = ['vcs', 'path', 'composer']; public function __construct( private readonly ContainerInterface $container, @@ -44,12 +44,12 @@ public function configure(): void $this->addArgument( self::ARG_TYPE, InputArgument::REQUIRED, - 'Specify the type of the repository, e.g. vcs, path', + 'Specify the type of the repository, e.g. vcs, path, composer', ); $this->addArgument( self::ARG_URL, InputArgument::REQUIRED, - 'Specify the URL of the repository, e.g. a Github/Gitlab URL, or a filesystem path', + 'Specify the URL of the repository, e.g. a Github/Gitlab URL, a filesystem path, or Private Packagist URL', ); $this->addUsage('lol'); } @@ -60,7 +60,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $pieJsonFilename = Platform::getPieJsonFilename($targetPlatform); $type = (string) $input->getArgument(self::ARG_TYPE); - /** @psalm-var 'vcs'|'path' $type */ + /** @psalm-var 'vcs'|'path'|'composer' $type */ Assert::inArray($type, self::ALLOWED_TYPES); $url = $originalUrl = (string) $input->getArgument(self::ARG_URL); From dcdd07650ea693f8ff4055bfad39b4478fe3012c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 23 Jan 2025 11:08:58 +0000 Subject: [PATCH 10/15] Allow adding/removing Packagist.org repo --- docs/usage.md | 2 ++ src/Command/RepositoryAddCommand.php | 12 ++++++++--- src/Command/RepositoryRemoveCommand.php | 11 ++++++++-- src/ComposerIntegration/PieJsonEditor.php | 16 +++++++++++++++ .../RepositoryManagementCommandsTest.php | 20 +++++++++++++++++++ .../ComposerIntegration/PieJsonEditorTest.php | 19 ++++++++++++++++++ 6 files changed, 75 insertions(+), 5 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index 209411f3..dd10cdb4 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -239,6 +239,7 @@ to use some other repository types: * `pie repository:add [--with-php-config=...] path /path/to/your/local/extension` * `pie repository:add [--with-php-config=...] vcs https://github.com/youruser/yourextension` * `pie repository:add [--with-php-config=...] composer https://repo.packagist.com/your-private-packagist/` +* `pie repository:add [--with-php-config=...] composer packagist.org` The `repository:*` commands all support the optional `--with-php-config` flag to allow you to specify which PHP installation to use (for example, if you have @@ -248,6 +249,7 @@ removed too, using the inverse `repository:remove` commands: * `pie repository:remove [--with-php-config=...] /path/to/your/local/extension` * `pie repository:remove [--with-php-config=...] https://github.com/youruser/yourextension` * `pie repository:remove [--with-php-config=...] https://repo.packagist.com/your-private-packagist/` +* `pie repository:remove [--with-php-config=...] packagist.org` Note you do not need to specify the repository type in `repository:remove`, just the URL. diff --git a/src/Command/RepositoryAddCommand.php b/src/Command/RepositoryAddCommand.php index 09852a34..fedcb6a4 100644 --- a/src/Command/RepositoryAddCommand.php +++ b/src/Command/RepositoryAddCommand.php @@ -17,6 +17,7 @@ use Webmozart\Assert\Assert; use function realpath; +use function str_contains; #[AsCommand( name: 'repository:add', @@ -69,16 +70,21 @@ public function execute(InputInterface $input, OutputInterface $output): int $url = realpath($originalUrl); } - Assert::stringNotEmpty($url, 'Could not resolve ' . $originalUrl . ' to a real path'); + if ($type === 'composer' && str_contains($url, 'packagist.org')) { + // "adding packagist" is really just removing an exclusion + (new PieJsonEditor($pieJsonFilename))->removeRepository('packagist.org'); + } else { + Assert::stringNotEmpty($url, 'Could not resolve ' . $originalUrl . ' to a real path'); - (new PieJsonEditor($pieJsonFilename))->addRepository($type, $url); + (new PieJsonEditor($pieJsonFilename))->addRepository($type, $url); + } CommandHelper::listRepositories( PieComposerFactory::createPieComposer( $this->container, PieComposerRequest::noOperation( $output, - CommandHelper::determineTargetPlatformFromInputs($input, $output), + $targetPlatform, ), ), $output, diff --git a/src/Command/RepositoryRemoveCommand.php b/src/Command/RepositoryRemoveCommand.php index bd9966fb..1742a718 100644 --- a/src/Command/RepositoryRemoveCommand.php +++ b/src/Command/RepositoryRemoveCommand.php @@ -16,6 +16,8 @@ use Symfony\Component\Console\Output\OutputInterface; use Webmozart\Assert\Assert; +use function str_contains; + #[AsCommand( name: 'repository:remove', description: 'Remove a repository for packages that PIE can use.', @@ -52,14 +54,19 @@ public function execute(InputInterface $input, OutputInterface $output): int $url = (string) $input->getArgument(self::ARG_URL); Assert::stringNotEmpty($url); - (new PieJsonEditor($pieJsonFilename))->removeRepository($url); + if (str_contains($url, 'packagist.org')) { + // "removing packagist" is really just adding an exclusion + (new PieJsonEditor($pieJsonFilename))->excludePackagistOrg(); + } else { + (new PieJsonEditor($pieJsonFilename))->removeRepository($url); + } CommandHelper::listRepositories( PieComposerFactory::createPieComposer( $this->container, PieComposerRequest::noOperation( $output, - CommandHelper::determineTargetPlatformFromInputs($input, $output), + $targetPlatform, ), ), $output, diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index a567b20b..47b8308b 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -15,6 +15,8 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class PieJsonEditor { + public const PACKAGIST_ORG_KEY = 'packagist.org'; + public function __construct(private readonly string $pieJsonFilename) { } @@ -60,6 +62,20 @@ public function revert(string $originalPieJsonContent): void file_put_contents($this->pieJsonFilename, $originalPieJsonContent); } + public function excludePackagistOrg(): string + { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + )) + ->addRepository(self::PACKAGIST_ORG_KEY, false); + + return $originalPieJsonContent; + } + /** * Add a repository to the given `pie.json`. Returns the original * `pie.json` content, in case it needs to be restored later. diff --git a/test/integration/Command/RepositoryManagementCommandsTest.php b/test/integration/Command/RepositoryManagementCommandsTest.php index de9de3ec..ffbf21a9 100644 --- a/test/integration/Command/RepositoryManagementCommandsTest.php +++ b/test/integration/Command/RepositoryManagementCommandsTest.php @@ -28,6 +28,7 @@ final class RepositoryManagementCommandsTest extends TestCase { private const EXAMPLE_PATH_REPOSITORY_URL = __DIR__; private const EXAMPLE_VCS_REPOSITORY_URL = 'https://github.com/asgrim/example-pie-extension'; + private const PACKAGIST_ORG_URL = 'packagist.org'; private CommandTester $listCommand; private CommandTester $addCommand; @@ -39,6 +40,10 @@ public function setUp(): void $this->addCommand = new CommandTester(Container::factory()->get(RepositoryAddCommand::class)); $this->removeCommand = new CommandTester(Container::factory()->get(RepositoryRemoveCommand::class)); + $this->addCommand->execute([ + 'type' => 'composer', + 'url' => self::PACKAGIST_ORG_URL, + ]); $this->removeCommand->execute(['url' => self::EXAMPLE_PATH_REPOSITORY_URL]); $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL]); $this->removeCommand->execute(['url' => self::EXAMPLE_VCS_REPOSITORY_URL . '.git']); @@ -84,6 +89,21 @@ public function testVcsRepositoriesCanBeManaged(): void $this->assertRepositoryListDisplayed(['Packagist']); } + public function testPackagistOrgCanBeManaged(): void + { + $this->assertRepositoryListDisplayed(['Packagist']); + + $this->removeCommand->execute(['url' => self::PACKAGIST_ORG_URL]); + + $this->assertRepositoryListDisplayed([]); + + $this->addCommand->execute([ + 'type' => 'composer', + 'url' => self::PACKAGIST_ORG_URL, + ]); + $this->assertRepositoryListDisplayed(['Packagist']); + } + /** @param list $expectedRepositories */ private function assertRepositoryListDisplayed(array $expectedRepositories): void { diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php index 9ceb3f69..1ab4641b 100644 --- a/test/unit/ComposerIntegration/PieJsonEditorTest.php +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -107,10 +107,29 @@ public function testCanAddAndRemoveRepositories(): void $this->normaliseJson($originalContent2), ); + $noRepositoriesContent = $this->normaliseJson(<<<'EOF' + { + "repositories": { + } + } + EOF); + + self::assertSame( + $noRepositoriesContent, + $this->normaliseJson(file_get_contents($testPieJson)), + ); + + $originalContent3 = $editor->excludePackagistOrg(); + self::assertSame( + $noRepositoriesContent, + $this->normaliseJson($originalContent3), + ); + self::assertSame( $this->normaliseJson(<<<'EOF' { "repositories": { + "packagist.org": false } } EOF), From 688b47c7a95b6be3a92f5f2fd0e63e2ef7953aa2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 24 Jan 2025 10:33:50 +0000 Subject: [PATCH 11/15] Added styling for GH Alert markdown syntax --- .github/docs/Dockerfile | 2 +- .github/docs/templates/online.twig | 35 ++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/.github/docs/Dockerfile b/.github/docs/Dockerfile index 9ca685a1..00d58d57 100644 --- a/.github/docs/Dockerfile +++ b/.github/docs/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/roave/docbooktool:1.16.0 AS builder +FROM ghcr.io/roave/docbooktool:1.19.0 AS builder COPY ./.github/docs/templates /docs-src/templates COPY ./docs /docs-src/book diff --git a/.github/docs/templates/online.twig b/.github/docs/templates/online.twig index efc704af..6a3f5f4a 100644 --- a/.github/docs/templates/online.twig +++ b/.github/docs/templates/online.twig @@ -15,6 +15,41 @@ .hidden { display: none; } + .markdown-alert { + border-left: 0.25em solid black; + padding: 1em 1em 0.25em; + background-color: #f7f7f7; + } + .markdown-alert-note { + border-left-color: #0d68d5; + } + .markdown-alert-note .markdown-alert-title { + color: #0d68d5; + } + .markdown-alert-tip { + border-left-color: #188337; + } + .markdown-alert-tip .markdown-alert-title { + color: #188337; + } + .markdown-alert-important { + border-left-color: #7844d6; + } + .markdown-alert-important .markdown-alert-title { + color: #7844d6; + } + .markdown-alert-warning { + border-left-color: #a67003; + } + .markdown-alert-warning .markdown-alert-title { + color: #a67003; + } + .markdown-alert-caution { + border-left-color: #d61c28; + } + .markdown-alert-caution .markdown-alert-title { + color: #d61c28; + } From 40b151360f2890448a609311ee7d82aec4de6ed4 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 24 Jan 2025 10:56:35 +0000 Subject: [PATCH 12/15] Improvements to table/code styling --- .github/docs/templates/online.twig | 8 ++++++++ docs/extension-maintainers.md | 2 +- docs/usage.md | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/docs/templates/online.twig b/.github/docs/templates/online.twig index 6a3f5f4a..16845a96 100644 --- a/.github/docs/templates/online.twig +++ b/.github/docs/templates/online.twig @@ -5,6 +5,8 @@ PIE Documentation + +