diff --git a/AGENTS.md b/AGENTS.md index cc9db68..7a338fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ Dirigent is a free and open package registry for Composer, the PHP package manag ### Project - Environment variables are stored in `.env.dirigent` and `.env.dirigent.*` (not `.env`). +- When adding new user-facing text, add the corresponding entries to the relevant file in `translations/` (e.g. `translations/messages.en.yaml`). Keep keys in their existing section and alphabetical order. ### PHP diff --git a/migrations/Version20260519093651.php b/migrations/Version20260519093651.php new file mode 100644 index 0000000..758f3b9 --- /dev/null +++ b/migrations/Version20260519093651.php @@ -0,0 +1,36 @@ +addSql(<<<'SQL' + ALTER TABLE version ADD pinned BOOLEAN DEFAULT NULL + SQL); + $this->addSql(<<<'SQL' + UPDATE version SET pinned = false + SQL); + $this->addSql(<<<'SQL' + ALTER TABLE version ALTER pinned SET NOT NULL + SQL); + } + + public function down(Schema $schema): void + { + $this->addSql(<<<'SQL' + ALTER TABLE version DROP pinned + SQL); + } +} diff --git a/src/Controller/Dashboard/DashboardPackagesInfoController.php b/src/Controller/Dashboard/DashboardPackagesInfoController.php index 18d2f00..c1a184d 100644 --- a/src/Controller/Dashboard/DashboardPackagesInfoController.php +++ b/src/Controller/Dashboard/DashboardPackagesInfoController.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Component\Security\Http\Attribute\IsGranted; class DashboardPackagesInfoController extends AbstractController { @@ -90,7 +91,7 @@ public function versionMetadataList( #[MapPackage] Package $package, #[MapPackage] Version $version, ): Response { - $metadataCollection = $this->metadataRepository->getMetadataCollectionForVersion($version); + $metadataCollection = $this->metadataRepository->findAllMetadataForVersion($version); return $this->render('dashboard/packages/package_version_revisions.html.twig', [ 'package' => $package, @@ -100,6 +101,46 @@ public function versionMetadataList( ]); } + #[Route('/packages/{package}/pin-metadata/{version}', name: 'dashboard_packages_version_pin', requirements: ['package' => MapPackage::PACKAGE_REGEX, 'version' => '.*'], methods: ['POST'])] + #[IsGranted('ROLE_ADMIN')] + public function pinMetadata( + Request $request, + #[MapPackage] Package $package, + #[MapPackage] Version $version, + ): Response { + $action = (string) $request->request->get('action'); + + if (!$this->isCsrfTokenValid($action . '-revision-' . $version->getId(), (string) $request->request->get('_token'))) { + throw $this->createAccessDeniedException(); + } + + if ('pin' === $action) { + $revision = $request->request->getInt('revision'); + if (null === $metadata = $this->metadataRepository->findMetadataForVersion($version, $revision)) { + throw $this->createNotFoundException('The revision does not exist.'); + } + + $version->setCurrentMetadata($metadata); + $version->setPinned(true); + } elseif ('unpin' === $action) { + if (null === $latestMetadata = $this->metadataRepository->findLatestMetadataForVersion($version)) { + throw $this->createNotFoundException('No metadata available for this version.'); + } + + $version->setCurrentMetadata($latestMetadata); + $version->setPinned(false); + } else { + throw $this->createNotFoundException('Invalid action.'); + } + + $this->entityManager->flush(); + + return $this->redirectToRoute('dashboard_packages_version_info', [ + 'package' => $package->getName(), + 'version' => $version->getName(), + ]); + } + #[Route('/packages/{package}/versions', name: 'dashboard_packages_versions', requirements: ['package' => MapPackage::PACKAGE_REGEX])] #[IsGrantedAccess] public function versions(#[MapPackage] Package $package): Response diff --git a/src/Doctrine/Entity/Version.php b/src/Doctrine/Entity/Version.php index 9acac43..796f9be 100644 --- a/src/Doctrine/Entity/Version.php +++ b/src/Doctrine/Entity/Version.php @@ -23,6 +23,12 @@ class Version extends TrackedEntity implements \Stringable #[ORM\Column] private bool $development; + /** + * Whether the current metadata is pinned. + */ + #[ORM\Column] + private bool $pinned = false; + #[ORM\Column] private bool $pruned = false; @@ -97,6 +103,16 @@ public function setDevelopment(bool $development): void $this->development = $development; } + public function isPinned(): bool + { + return $this->pinned; + } + + public function setPinned(bool $pinned): void + { + $this->pinned = $pinned; + } + public function isPruned(): bool { return $this->pruned; diff --git a/src/Doctrine/Repository/MetadataRepository.php b/src/Doctrine/Repository/MetadataRepository.php index 9622a89..08100ee 100644 --- a/src/Doctrine/Repository/MetadataRepository.php +++ b/src/Doctrine/Repository/MetadataRepository.php @@ -48,11 +48,26 @@ public function getMetadataCountForVersion(Version $version): int return $this->count(['version' => $version]); } - public function getMetadataCollectionForVersion(Version $version): array + public function findMetadataForVersion(Version $version, int $revision): ?Metadata + { + return $this->findOneBy( + criteria: ['version' => $version, 'revision' => $revision], + ); + } + + public function findLatestMetadataForVersion(Version $version): ?Metadata + { + return $this->findOneBy( + criteria: ['version' => $version], + orderBy: ['revision' => Order::Descending->value], + ); + } + + public function findAllMetadataForVersion(Version $version): array { return $this->findBy( - ['version' => $version], - ['revision' => Order::Descending->value], + criteria: ['version' => $version], + orderBy: ['revision' => Order::Descending->value], ); } diff --git a/src/Package/PackageMetadataResolver.php b/src/Package/PackageMetadataResolver.php index df0fd63..1d55d75 100644 --- a/src/Package/PackageMetadataResolver.php +++ b/src/Package/PackageMetadataResolver.php @@ -213,7 +213,7 @@ private function updatePackage(Package $package, array $composerPackages, ?VcsDr // Remove outdated versions foreach ($existingVersionMetadata as $version) { - $removeVersion = $version->isDevelopment() ? !$this->retainPrunedVersionsDev : !$this->retainPrunedVersionsTagged; + $removeVersion = !$version->isPinned() && ($version->isDevelopment() ? !$this->retainPrunedVersionsDev : !$this->retainPrunedVersionsTagged); if ($removeVersion) { $this->entityManager->remove($version); } elseif (!$version->isPruned()) { @@ -236,13 +236,17 @@ private function updateVersion(Version $version, CompletePackageInterface $data, if (null === $currentMetadata || $this->hasMetadataChanged($currentMetadata, $metadata)) { $metadata->setRevision($version->getNextRevision(increment: true)); - $version->setCurrentMetadata($metadata); $this->entityManager->persist($metadata); - $removePreviousMetadata = $version->isDevelopment() ? !$this->retainStaleRevisionsDev : !$this->retainStaleRevisionsTagged; - if (null !== $currentMetadata && $removePreviousMetadata) { - $this->entityManager->remove($currentMetadata); + $retainStaleRevisions = $version->isDevelopment() ? $this->retainStaleRevisionsDev : $this->retainStaleRevisionsTagged; + if (!$version->isPinned() || !$retainStaleRevisions) { + $version->setCurrentMetadata($metadata); + $version->setPinned(false); + + if (null !== $currentMetadata && !$retainStaleRevisions) { + $this->entityManager->remove($currentMetadata); + } } } diff --git a/templates/dashboard/packages/package_info.html.twig b/templates/dashboard/packages/package_info.html.twig index 2a471cb..1fcd771 100644 --- a/templates/dashboard/packages/package_info.html.twig +++ b/templates/dashboard/packages/package_info.html.twig @@ -120,16 +120,29 @@ '%version%': "#{version.name|escape}", })|raw }}
- {% if not metadata.isCurrentMetadata %} -- - {{ "You're viewing an inactive revision of this package"|trans }} - -
+ {% if not metadata.isCurrentMetadata or version.pinned %} +