diff --git a/composer.json b/composer.json index c6742bfb..92865380 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "excelwebzone/recaptcha-bundle": "^1.5", "knplabs/github-api": "^2.12", "knpuniversity/oauth2-client-bundle": "^2.0", + "league/commonmark": "^1.5", "league/flysystem-bundle": "^1.5", "league/oauth2-github": "^2.0", "m4tthumphrey/php-gitlab-api": "^9.17", diff --git a/composer.lock b/composer.lock index 54d27fdf..16ec354a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5851bfd200af8705c7345589054c60b0", + "content-hash": "cd9ebb11af8dd1c8d58854c9287c4bf8", "packages": [ { "name": "bitbucket/client", @@ -130,10 +130,6 @@ "brick", "math" ], - "support": { - "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/master" - }, "funding": [ { "url": "https://tidelift.com/funding/github/packagist/brick/math", @@ -179,7 +175,7 @@ "Buddy\\": "src/Buddy/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "Apache-2.0" ], @@ -245,7 +241,7 @@ "Buddy\\OAuth2\\Client\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -496,11 +492,6 @@ "ssl", "tls" ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.8" - }, "funding": [ { "url": "https://packagist.com", @@ -676,10 +667,6 @@ } ], "description": "Composer plugin that provides efficient querying for installed package versions (no runtime IO)", - "support": { - "issues": "https://github.com/composer/package-versions-deprecated/issues", - "source": "https://github.com/composer/package-versions-deprecated/tree/master" - }, "funding": [ { "url": "https://packagist.com", @@ -905,11 +892,6 @@ "Xdebug", "performance" ], - "support": { - "irc": "irc://irc.freenode.org/composer", - "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.3" - }, "funding": [ { "url": "https://packagist.com", @@ -1262,6 +1244,20 @@ "doctrine", "php" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcommon", + "type": "tidelift" + } + ], "time": "2020-06-05T16:46:05+00:00" }, { @@ -1565,10 +1561,6 @@ "migrations", "schema" ], - "support": { - "issues": "https://github.com/doctrine/DoctrineMigrationsBundle/issues", - "source": "https://github.com/doctrine/DoctrineMigrationsBundle/tree/3.0.x" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -1763,6 +1755,20 @@ "uppercase", "words" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], "time": "2020-05-29T07:19:59+00:00" }, { @@ -1825,6 +1831,20 @@ "constructor", "instantiate" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -1893,6 +1913,20 @@ "parser", "php" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], "time": "2020-05-25T17:44:05+00:00" }, { @@ -1984,10 +2018,6 @@ "migrations", "php" ], - "support": { - "issues": "https://github.com/doctrine/migrations/issues", - "source": "https://github.com/doctrine/migrations/tree/3.0.1" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -2262,7 +2292,7 @@ "Doctrine\\Common\\": "lib/Doctrine/Common" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -2470,10 +2500,6 @@ "event-dispatcher", "event-emitter" ], - "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/master" - }, "time": "2017-07-23T21:35:13+00:00" }, { @@ -2835,7 +2861,7 @@ "Http\\Factory\\Guzzle\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -3182,7 +3208,7 @@ "Laminas\\Code\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "BSD-3-Clause" ], @@ -3256,14 +3282,6 @@ "events", "laminas" ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-eventmanager/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-eventmanager/issues", - "rss": "https://github.com/laminas/laminas-eventmanager/releases.atom", - "source": "https://github.com/laminas/laminas-eventmanager" - }, "funding": [ { "url": "https://funding.communitybridge.org/projects/laminas-project", @@ -3336,6 +3354,107 @@ ], "time": "2020-08-18T16:34:51+00:00" }, + { + "name": "league/commonmark", + "version": "1.5.6", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "a56e91e0fa1f6d0049153a9c34f63488f6b7ce61" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/a56e91e0fa1f6d0049153a9c34f63488f6b7ce61", + "reference": "a56e91e0fa1f6d0049153a9c34f63488f6b7ce61", + "shasum": "", + "mirrors": [ + { + "url": "https://repo.repman.io/dists/%package%/%version%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "scrutinizer/ocular": "1.7.*" + }, + "require-dev": { + "cebe/markdown": "~1.0", + "commonmark/commonmark.js": "0.29.2", + "erusev/parsedown": "~1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "~1.4", + "mikehaertl/php-shellcommand": "^1.4", + "phpstan/phpstan": "^0.12", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.2", + "scrutinizer/ocular": "^1.5", + "symfony/finder": "^4.2" + }, + "bin": [ + "bin/commonmark" + ], + "type": "library", + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://repo.repman.io/downloads", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and Github-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "funding": [ + { + "url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark", + "type": "custom" + }, + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://www.patreon.com/colinodell", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2020-10-17T21:33:03+00:00" + }, { "name": "league/flysystem", "version": "1.1.3", @@ -3425,10 +3544,6 @@ "sftp", "storage" ], - "support": { - "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/1.x" - }, "funding": [ { "url": "https://offset.earth/frankdejonge", @@ -3505,10 +3620,6 @@ } ], "description": "Symfony bundle integrating Flysystem into Symfony 4.2+ applications", - "support": { - "issues": "https://github.com/thephpleague/flysystem-bundle/issues", - "source": "https://github.com/thephpleague/flysystem-bundle/tree/1.5.0" - }, "time": "2020-04-04T22:09:59+00:00" }, { @@ -3680,7 +3791,7 @@ "League\\OAuth2\\Client\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -3774,10 +3885,6 @@ "api", "gitlab" ], - "support": { - "issues": "https://github.com/GitLabPHP/Client/issues", - "source": "https://github.com/GitLabPHP/Client/tree/9.19.0" - }, "funding": [ { "url": "https://github.com/GrahamCampbell", @@ -4254,10 +4361,6 @@ "oauth", "oauth2" ], - "support": { - "issues": "https://github.com/omines/oauth2-gitlab/issues", - "source": "https://github.com/omines/oauth2-gitlab/tree/3.3.0" - }, "time": "2020-08-28T09:55:21+00:00" }, { @@ -4716,7 +4819,7 @@ "Http\\Message\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5058,7 +5161,7 @@ "Psr\\Cache\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5110,7 +5213,7 @@ "Psr\\Container\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5165,7 +5268,7 @@ "Psr\\EventDispatcher\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5273,7 +5376,7 @@ "Psr\\Http\\Message\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5330,7 +5433,7 @@ "Psr\\Http\\Message\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5386,7 +5489,7 @@ "Psr\\Log\\": "Psr/Log/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5438,7 +5541,7 @@ "src/getallheaders.php" ] }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5611,11 +5714,6 @@ "identifier", "uuid" ], - "support": { - "issues": "https://github.com/ramsey/uuid/issues", - "rss": "https://github.com/ramsey/uuid/releases.atom", - "source": "https://github.com/ramsey/uuid" - }, "funding": [ { "url": "https://github.com/ramsey", @@ -5661,7 +5759,7 @@ "Ramsey\\Uuid\\Doctrine\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -5818,10 +5916,6 @@ "asynchronous", "event-loop" ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.1.1" - }, "time": "2020-01-01T18:39:52+00:00" }, { @@ -5960,10 +6054,6 @@ "promise", "promises" ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v2.8.0" - }, "time": "2020-05-12T15:16:56+00:00" }, { @@ -6026,10 +6116,6 @@ "stream", "unwrap" ], - "support": { - "issues": "https://github.com/reactphp/promise-stream/issues", - "source": "https://github.com/reactphp/promise-stream/tree/v1.2.0" - }, "time": "2019-07-03T12:29:10+00:00" }, { @@ -6089,10 +6175,6 @@ "timeout", "timer" ], - "support": { - "issues": "https://github.com/reactphp/promise-timer/issues", - "source": "https://github.com/reactphp/promise-timer/tree/v1.6.0" - }, "time": "2020-07-10T12:18:06+00:00" }, { @@ -6169,10 +6251,6 @@ "reactphp", "stream" ], - "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.6.0" - }, "funding": [ { "url": "https://github.com/WyriHaximus", @@ -6235,10 +6313,6 @@ "stream", "writable" ], - "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.1.1" - }, "time": "2020-05-04T10:17:57+00:00" }, { @@ -6303,9 +6377,6 @@ "stream", "uri" ], - "support": { - "source": "https://github.com/ringcentral/psr7/tree/master" - }, "time": "2018-05-29T20:21:04+00:00" }, { @@ -6361,10 +6432,6 @@ "parser", "validator" ], - "support": { - "issues": "https://github.com/Seldaek/jsonlint/issues", - "source": "https://github.com/Seldaek/jsonlint/tree/master" - }, "funding": [ { "url": "https://github.com/Seldaek", @@ -6507,10 +6574,6 @@ "annotations", "controllers" ], - "support": { - "issues": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/issues", - "source": "https://github.com/sensiolabs/SensioFrameworkExtraBundle/tree/v5.6.1" - }, "time": "2020-08-25T19:10:18+00:00" }, { @@ -6794,7 +6857,7 @@ "Stevenmaguire\\OAuth2\\Client\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "notification-url": "https://repo.repman.io/downloads", "license": [ "MIT" ], @@ -12332,10 +12395,6 @@ "safe writer", "webimpress" ], - "support": { - "issues": "https://github.com/webimpress/safe-writer/issues", - "source": "https://github.com/webimpress/safe-writer/tree/master" - }, "funding": [ { "url": "https://github.com/michalbundyra", @@ -12734,10 +12793,6 @@ "keywords": [ "database" ], - "support": { - "issues": "https://github.com/doctrine/data-fixtures/issues", - "source": "https://github.com/doctrine/data-fixtures/tree/1.4.x" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -12826,10 +12881,6 @@ "Fixture", "persistence" ], - "support": { - "issues": "https://github.com/doctrine/DoctrineFixturesBundle/issues", - "source": "https://github.com/doctrine/DoctrineFixturesBundle/tree/3.3.x" - }, "funding": [ { "url": "https://www.doctrine-project.org/sponsorship.html", @@ -13073,66 +13124,6 @@ ], "time": "2019-12-12T13:22:17+00:00" }, - { - "name": "jdorn/sql-formatter", - "version": "v1.2.17", - "source": { - "type": "git", - "url": "https://github.com/jdorn/sql-formatter.git", - "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/jdorn/sql-formatter/zipball/64990d96e0959dff8e059dfcdc1af130728d92bc", - "reference": "64990d96e0959dff8e059dfcdc1af130728d92bc", - "shasum": "", - "mirrors": [ - { - "url": "https://repo.repman.io/dists/%package%/%version%/%reference%.%type%", - "preferred": true - } - ] - }, - "require": { - "php": ">=5.2.4" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.3.x-dev" - } - }, - "autoload": { - "classmap": [ - "lib" - ] - }, - "notification-url": "https://repo.repman.io/downloads", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jeremy Dorn", - "email": "jeremy@jeremydorn.com", - "homepage": "http://jeremydorn.com/" - } - ], - "description": "a PHP SQL highlighting library", - "homepage": "https://github.com/jdorn/sql-formatter/", - "keywords": [ - "highlight", - "sql" - ], - "support": { - "issues": "https://github.com/jdorn/sql-formatter/issues", - "source": "https://github.com/jdorn/sql-formatter/tree/master" - }, - "time": "2014-01-12T16:20:24+00:00" - }, { "name": "league/flysystem-memory", "version": "1.0.2", @@ -13188,10 +13179,6 @@ "adapter", "memory" ], - "support": { - "issues": "https://github.com/thephpleague/flysystem-memory/issues", - "source": "https://github.com/thephpleague/flysystem-memory/tree/1.0.2" - }, "time": "2019-05-30T21:34:13+00:00" }, { @@ -13656,10 +13643,6 @@ "MIT" ], "description": "Composer plugin for automatic installation of PHPStan extensions", - "support": { - "issues": "https://github.com/phpstan/extension-installer/issues", - "source": "https://github.com/phpstan/extension-installer/tree/1.0.5" - }, "time": "2020-08-30T12:06:42+00:00" }, { @@ -13970,10 +13953,6 @@ "MIT" ], "description": "Extra strict and opinionated rules for PHPStan", - "support": { - "issues": "https://github.com/phpstan/phpstan-strict-rules/issues", - "source": "https://github.com/phpstan/phpstan-strict-rules/tree/master" - }, "time": "2020-08-30T15:42:06+00:00" }, { @@ -14422,6 +14401,16 @@ "testing", "xunit" ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-06-22T07:06:58+00:00" }, { @@ -15471,10 +15460,6 @@ "scaffold", "scaffolding" ], - "support": { - "issues": "https://github.com/symfony/maker-bundle/issues", - "source": "https://github.com/symfony/maker-bundle/tree/master" - }, "funding": [ { "url": "https://symfony.com/sponsor", diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 5c318302..9b1affc4 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -28,6 +28,7 @@ use Buddy\Repman\Query\User\Model\Organization; use Buddy\Repman\Query\User\Model\Organization\Token; use Buddy\Repman\Query\User\Model\Package; +use Buddy\Repman\Query\User\Model\PackageDetails; use Buddy\Repman\Query\User\OrganizationQuery; use Buddy\Repman\Query\User\PackageQuery; use Buddy\Repman\Query\User\PackageQuery\Filter as PackageFilter; @@ -108,7 +109,7 @@ public function removePackage(Organization $organization, Package $package): Res /** * @Route("/organization/{organization}/package/{package}/details", name="organization_package_details", methods={"GET"}, requirements={"organization"="%organization_pattern%","package"="%uuid_pattern%"}) */ - public function packageDetails(Organization $organization, Package $package, Request $request): Response + public function packageDetails(Organization $organization, PackageDetails $package, Request $request): Response { $filter = Filter::fromRequest($request); diff --git a/src/Entity/Organization/Package.php b/src/Entity/Organization/Package.php index cbb3e02e..785d1189 100644 --- a/src/Entity/Organization/Package.php +++ b/src/Entity/Organization/Package.php @@ -86,6 +86,11 @@ class Package */ private array $metadata; + /** + * @ORM\Column(type="text", nullable=true) + */ + private ?string $readme = null; + /** * @ORM\Column(type="json", nullable=true) * @@ -247,6 +252,16 @@ public function metadata(string $key) return $this->metadata[$key]; } + public function readme(): ?string + { + return $this->readme; + } + + public function setReadme(?string $readme): void + { + $this->readme = $readme; + } + public function latestReleasedVersion(): ?string { return $this->latestReleasedVersion; diff --git a/src/Migrations/Version20201014155748.php b/src/Migrations/Version20201014155748.php new file mode 100644 index 00000000..420f4a70 --- /dev/null +++ b/src/Migrations/Version20201014155748.php @@ -0,0 +1,26 @@ +addSql('ALTER TABLE "organization_package" ADD readme text default null'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE "organization_package" DROP readme'); + } +} diff --git a/src/Query/User/Model/Package.php b/src/Query/User/Model/Package.php index 66081f88..e433f06c 100644 --- a/src/Query/User/Model/Package.php +++ b/src/Query/User/Model/Package.php @@ -6,6 +6,8 @@ final class Package { + use PackageScanResultTrait; + private string $id; private string $organizationId; private string $type; @@ -17,7 +19,6 @@ final class Package private ?\DateTimeImmutable $lastSyncAt; private ?string $lastSyncError; private ?\DateTimeImmutable $webhookCreatedAt; - private ?ScanResult $scanResult; private int $keepLastReleases; public function __construct( @@ -115,39 +116,6 @@ public function isSynchronizedSuccessfully(): bool return $this->name() !== null && $this->lastSyncError() === null; } - public function scanResultStatus(): string - { - return $this->scanResult !== null ? $this->scanResult->status() : ScanResult::statusPending(); - } - - public function scanResultDate(): ?\DateTimeImmutable - { - return $this->scanResult !== null ? $this->scanResult->date() : null; - } - - public function isScanResultOk(): ?bool - { - return $this->scanResult !== null ? $this->scanResult->isOk() : false; - } - - public function isScanResultPending(): bool - { - return $this->scanResult !== null ? $this->scanResult->isPending() : true; - } - - public function isScanResultNotAvailable(): bool - { - return $this->scanResult !== null ? $this->scanResult->isNotAvailable() : true; - } - - /** - * @return mixed[] - */ - public function lastScanResultContent(): array - { - return $this->scanResult !== null ? $this->scanResult->content() : []; - } - public function keepLastReleases(): int { return $this->keepLastReleases; diff --git a/src/Query/User/Model/PackageDetails.php b/src/Query/User/Model/PackageDetails.php new file mode 100644 index 00000000..f2a7176c --- /dev/null +++ b/src/Query/User/Model/PackageDetails.php @@ -0,0 +1,94 @@ +id = $id; + $this->organizationId = $organizationId; + $this->name = $name; + $this->latestReleasedVersion = $latestReleasedVersion; + $this->latestReleaseDate = $latestReleaseDate; + $this->description = $description; + $this->lastSyncError = $lastSyncError; + $this->scanResult = $scanResult ?? null; + $this->keepLastReleases = $keepLastReleases; + $this->readme = $readme; + } + + public function id(): string + { + return $this->id; + } + + public function organizationId(): string + { + return $this->organizationId; + } + + public function name(): ?string + { + return $this->name; + } + + public function description(): ?string + { + return $this->description; + } + + public function latestReleasedVersion(): ?string + { + return $this->latestReleasedVersion; + } + + public function latestReleaseDate(): ?\DateTimeImmutable + { + return $this->latestReleaseDate; + } + + public function getKeepLastReleases(): int + { + return $this->keepLastReleases; + } + + public function getReadme(): ?string + { + return $this->readme; + } + + public function lastSyncError(): ?string + { + return $this->lastSyncError; + } + + public function isSynchronizedSuccessfully(): bool + { + return $this->name() !== null && $this->lastSyncError() === null; + } +} diff --git a/src/Query/User/Model/PackageScanResultTrait.php b/src/Query/User/Model/PackageScanResultTrait.php new file mode 100644 index 00000000..3fa90411 --- /dev/null +++ b/src/Query/User/Model/PackageScanResultTrait.php @@ -0,0 +1,43 @@ +scanResult !== null ? $this->scanResult->status() : ScanResult::statusPending(); + } + + public function scanResultDate(): ?\DateTimeImmutable + { + return $this->scanResult !== null ? $this->scanResult->date() : null; + } + + public function isScanResultOk(): ?bool + { + return $this->scanResult !== null ? $this->scanResult->isOk() : false; + } + + public function isScanResultPending(): bool + { + return $this->scanResult !== null ? $this->scanResult->isPending() : true; + } + + public function isScanResultNotAvailable(): bool + { + return $this->scanResult !== null ? $this->scanResult->isNotAvailable() : true; + } + + /** + * @return mixed[] + */ + public function lastScanResultContent(): array + { + return $this->scanResult !== null ? $this->scanResult->content() : []; + } +} diff --git a/src/Query/User/PackageQuery.php b/src/Query/User/PackageQuery.php index 916a9344..3267c620 100644 --- a/src/Query/User/PackageQuery.php +++ b/src/Query/User/PackageQuery.php @@ -7,6 +7,7 @@ use Buddy\Repman\Query\Filter; use Buddy\Repman\Query\User\Model\Installs; use Buddy\Repman\Query\User\Model\Package; +use Buddy\Repman\Query\User\Model\PackageDetails; use Buddy\Repman\Query\User\Model\PackageName; use Buddy\Repman\Query\User\Model\ScanResult; use Buddy\Repman\Query\User\Model\Version; @@ -33,6 +34,11 @@ public function getAllNames(string $organizationId): array; */ public function getById(string $id): Option; + /** + * @return Option + */ + public function getDetailsById(string $id): Option; + public function versionCount(string $packageId): int; /** diff --git a/src/Query/User/PackageQuery/DbalPackageQuery.php b/src/Query/User/PackageQuery/DbalPackageQuery.php index d43d6c22..e95ba227 100644 --- a/src/Query/User/PackageQuery/DbalPackageQuery.php +++ b/src/Query/User/PackageQuery/DbalPackageQuery.php @@ -8,6 +8,7 @@ use Buddy\Repman\Query\Filter as BaseFilter; use Buddy\Repman\Query\User\Model\Installs; use Buddy\Repman\Query\User\Model\Package; +use Buddy\Repman\Query\User\Model\PackageDetails; use Buddy\Repman\Query\User\Model\PackageName; use Buddy\Repman\Query\User\Model\ScanResult; use Buddy\Repman\Query\User\Model\Version; @@ -155,6 +156,40 @@ public function getById(string $id): Option return Option::some($this->hydratePackage($data)); } + /** + * @return Option + */ + public function getDetailsById(string $id): Option + { + $data = $this->connection->fetchAssoc( + 'SELECT + id, + organization_id, + type, + repository_url, + name, + latest_released_version, + latest_release_date, + description, + last_sync_at, + last_sync_error, + webhook_created_at, + last_scan_date, + last_scan_status, + last_scan_result, + keep_last_releases, + readme + FROM "organization_package" + WHERE id = :id', [ + ':id' => $id, + ]); + if ($data === false) { + return Option::none(); + } + + return Option::some($this->hydratePackageDetails($data)); + } + public function versionCount(string $packageId): int { return (int) $this @@ -340,6 +375,33 @@ private function hydratePackage(array $data): Package ); } + /** + * @param array $data + */ + private function hydratePackageDetails(array $data): PackageDetails + { + $scanResult = isset($data['last_scan_status']) ? + new ScanResult( + new \DateTimeImmutable($data['last_scan_date']), + $data['last_scan_status'], + $data['latest_released_version'], + $data['last_scan_result'], + ) : null; + + return new PackageDetails( + $data['id'], + $data['organization_id'], + $data['name'], + $data['latest_released_version'], + $data['latest_release_date'] !== null ? new \DateTimeImmutable($data['latest_release_date']) : null, + $data['description'], + $data['last_sync_error'], + $scanResult, + $data['keep_last_releases'] ?? 0, + $data['readme'] ?? null, + ); + } + /** * @return Version[] */ diff --git a/src/Service/Organization/PackageDetailsParamConverter.php b/src/Service/Organization/PackageDetailsParamConverter.php new file mode 100644 index 00000000..9ddb4de5 --- /dev/null +++ b/src/Service/Organization/PackageDetailsParamConverter.php @@ -0,0 +1,47 @@ +packageQuery = $packageQuery; + } + + public function supports(ParamConverter $configuration) + { + return $configuration->getClass() === PackageDetails::class; + } + + public function apply(Request $request, ParamConverter $configuration) + { + if (null === $id = $request->attributes->get('package')) { + throw new BadRequestHttpException('Missing package parameter in request'); + } + + /** @var PackageDetails $package */ + $package = $this->packageQuery->getDetailsById($id)->getOrElseThrow(new NotFoundHttpException('Package not found')); + $organization = $request->attributes->get('organization'); + if ($organization instanceof Organization && $package->organizationId() !== $organization->id()) { + throw new NotFoundHttpException('Package not found'); + } + + $request->attributes->set($configuration->getName(), $package); + + return true; + } +} diff --git a/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php b/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php index b5bf3520..2c2c8855 100644 --- a/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php +++ b/src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php @@ -12,11 +12,13 @@ use Buddy\Repman\Service\Organization\PackageManager; use Buddy\Repman\Service\PackageNormalizer; use Buddy\Repman\Service\PackageSynchronizer; +use Buddy\Repman\Service\ReadmeExtractor; use Composer\Config; use Composer\Factory; use Composer\IO\BufferIO; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; +use Composer\Package\PackageInterface; use Composer\Repository\RepositoryFactory; use Composer\Repository\RepositoryInterface; use Composer\Semver\Comparator; @@ -29,6 +31,7 @@ final class ComposerPackageSynchronizer implements PackageSynchronizer private PackageNormalizer $packageNormalizer; private PackageRepository $packageRepository; private Storage $distStorage; + private ReadmeExtractor $readmeExtractor; private string $gitlabUrl; public function __construct(PackageManager $packageManager, PackageNormalizer $packageNormalizer, PackageRepository $packageRepository, Storage $distStorage, string $gitlabUrl) @@ -38,6 +41,7 @@ public function __construct(PackageManager $packageManager, PackageNormalizer $p $this->packageRepository = $packageRepository; $this->distStorage = $distStorage; $this->gitlabUrl = $gitlabUrl; + $this->readmeExtractor = new ReadmeExtractor($this->distStorage); } public function synchronize(Package $package): void @@ -50,6 +54,14 @@ public function synchronize(Package $package): void $json = ['packages' => []]; $packages = $repository->getPackages(); + usort($packages, static function (PackageInterface $a, PackageInterface $b): int { + if ($a->getVersion() === $b->getVersion()) { + return $a->getReleaseDate() <=> $b->getReleaseDate(); + } + + return Comparator::greaterThan($a->getVersion(), $b->getVersion()) ? 1 : -1; + }); + if ($packages === []) { throw new \RuntimeException('Package not found'); } @@ -123,6 +135,10 @@ public function synchronize(Package $package): void $this->getAuthHeaders($package) ); + if ($latest->getVersion() === $version['version']) { + $this->readmeExtractor->extractReadme($package, $dist); + } + $package->addOrUpdateVersion( new Version( Uuid::uuid4(), diff --git a/src/Service/ReadmeExtractor.php b/src/Service/ReadmeExtractor.php new file mode 100644 index 00000000..1c339af0 --- /dev/null +++ b/src/Service/ReadmeExtractor.php @@ -0,0 +1,75 @@ +distStorage = $distStorage; + + $environment = Environment::createGFMEnvironment(); + $environment->addExtension(new ExternalLinkExtension()); + $environment->addExtension(new HeadingPermalinkExtension()); + + $this->markdownConverter = new CommonMarkConverter( + [ + 'allow_unsafe_links' => false, + 'external_link' => [ + 'open_in_new_window' => true, + 'nofollow' => '', + 'noopener' => 'external', + 'noreferrer' => 'external', + ], + 'heading_permalink' => [ + 'symbol' => '', + 'html_class' => '', + 'title' => '', + ], + ], + $environment + ); + } + + public function extractReadme(Package $package, Dist $dist): void + { + $package->setReadme($this->loadREADME($dist)); + } + + private function loadREADME(Dist $dist): ?string + { + $zipFilename = $this->distStorage->filename($dist); + + $zip = new \ZipArchive(); + $result = $zip->open($zipFilename); + if ($result !== true) { + return null; + } + + try { + for ($i = 0; $i < $zip->numFiles; ++$i) { + $filename = (string) $zip->getNameIndex($i); + if (preg_match('/^([^\/]+\/)?README.md$/i', $filename) === 1) { + return $this->markdownConverter->convertToHtml((string) $zip->getFromIndex($i)); + } + } + } finally { + $zip->close(); + } + + return null; + } +} diff --git a/symfony.lock b/symfony.lock index 400658be..0e1dad67 100644 --- a/symfony.lock +++ b/symfony.lock @@ -270,6 +270,9 @@ "laminas/laminas-zendframework-bridge": { "version": "1.0.1" }, + "league/commonmark": { + "version": "1.5.5" + }, "league/flysystem": { "version": "1.0.69" }, diff --git a/templates/organization/package/details.html.twig b/templates/organization/package/details.html.twig index 4f9376b5..5d6f302f 100644 --- a/templates/organization/package/details.html.twig +++ b/templates/organization/package/details.html.twig @@ -146,4 +146,16 @@ {% endif %} + {% if package.readme() %} +
+ +

README

+ +
+
+ {{ package.readme()|raw }} +
+
+ {% endif %} + {% endblock %} diff --git a/tests/Doubles/FakeDownloader.php b/tests/Doubles/FakeDownloader.php index eb2f7a56..62a65f1f 100644 --- a/tests/Doubles/FakeDownloader.php +++ b/tests/Doubles/FakeDownloader.php @@ -18,9 +18,9 @@ final class FakeDownloader implements Downloader */ private array $content = []; - public function __construct() + public function __construct(string $basePath = __DIR__.'/../Resources') { - $this->basePath = __DIR__.'/../Resources'; + $this->basePath = $basePath; } /** diff --git a/tests/Doubles/FakePackageSynchronizer.php b/tests/Doubles/FakePackageSynchronizer.php index 70083409..62ff18fd 100644 --- a/tests/Doubles/FakePackageSynchronizer.php +++ b/tests/Doubles/FakePackageSynchronizer.php @@ -15,6 +15,7 @@ final class FakePackageSynchronizer implements PackageSynchronizer private string $latestReleasedVersion = '1.0.0'; private \DateTimeImmutable $latestReleaseDate; private ?string $error = null; + private ?string $readme = null; /** * @var Version[] @@ -29,7 +30,7 @@ public function __construct() /** * @param Version[] $versions */ - public function setData(string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = []): void + public function setData(string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = [], ?string $readme = null): void { $this->name = $name; $this->description = $description; @@ -37,6 +38,7 @@ public function setData(string $name, string $description, string $latestRelease $this->latestReleaseDate = $latestReleaseDate; $this->error = null; $this->versions = $versions; + $this->readme = $readme; } public function setError(string $error): void @@ -52,6 +54,8 @@ public function synchronize(Package $package): void return; } + $package->setReadme($this->readme); + foreach ($this->versions as $version) { $package->addOrUpdateVersion($version); } diff --git a/tests/Functional/Controller/OrganizationControllerTest.php b/tests/Functional/Controller/OrganizationControllerTest.php index f33f4fff..3cbe5bad 100644 --- a/tests/Functional/Controller/OrganizationControllerTest.php +++ b/tests/Functional/Controller/OrganizationControllerTest.php @@ -444,7 +444,7 @@ public function testPackageDetails(): void new Version(Uuid::uuid4(), '1.0.1', 'ref2', 1048576, new \DateTimeImmutable(), Version::STABILITY_STABLE), new Version(Uuid::uuid4(), '1.1.0', 'lastref', 1073741824, new \DateTimeImmutable(), Version::STABILITY_STABLE), ]; - $this->fixtures->syncPackageWithData($packageId, 'buddy-works/buddy', 'Test', '1.1.1', new \DateTimeImmutable(), $versions); + $this->fixtures->syncPackageWithData($packageId, 'buddy-works/buddy', 'Test', '1.1.1', new \DateTimeImmutable(), $versions, 'This is a readme'); $this->client->request('GET', $this->urlTo('organization_package_details', [ 'organization' => 'buddy', @@ -459,6 +459,7 @@ public function testPackageDetails(): void self::assertStringContainsString($version->version(), $this->lastResponseBody()); self::assertStringContainsString($version->reference(), $this->lastResponseBody()); } + self::assertStringContainsString('This is a readme', $this->lastResponseBody()); } public function testPackageStats(): void diff --git a/tests/Integration/FixturesManager.php b/tests/Integration/FixturesManager.php index 7f3a16bb..a7fcb1dc 100644 --- a/tests/Integration/FixturesManager.php +++ b/tests/Integration/FixturesManager.php @@ -142,7 +142,7 @@ public function addPackage(string $orgId, string $url, string $type = 'vcs', arr $orgId, $url, $type, - $metadata + $metadata, ) ); @@ -197,9 +197,9 @@ public function syncPackageWithError(string $packageId, string $error): void /** * @param Version[] $versions */ - public function syncPackageWithData(string $packageId, string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = []): void + public function syncPackageWithData(string $packageId, string $name, string $description, string $latestReleasedVersion, \DateTimeImmutable $latestReleaseDate, array $versions = [], ?string $readme = null): void { - $this->container->get(PackageSynchronizer::class)->setData($name, $description, $latestReleasedVersion, $latestReleaseDate, $versions); + $this->container->get(PackageSynchronizer::class)->setData($name, $description, $latestReleasedVersion, $latestReleaseDate, $versions, $readme); $this->dispatchMessage(new SynchronizePackage($packageId)); $this->container->get(EntityManagerInterface::class)->flush(); } diff --git a/tests/Resources/readme-artifacts/no-readme/buddy-works-alpha-1.2.0.zip b/tests/Resources/readme-artifacts/no-readme/buddy-works-alpha-1.2.0.zip new file mode 100644 index 00000000..dfa754b6 Binary files /dev/null and b/tests/Resources/readme-artifacts/no-readme/buddy-works-alpha-1.2.0.zip differ diff --git a/tests/Resources/readme-artifacts/no-stable-release/buddy-works-alpha-1.3.0.zip b/tests/Resources/readme-artifacts/no-stable-release/buddy-works-alpha-1.3.0.zip new file mode 100644 index 00000000..26899ff0 Binary files /dev/null and b/tests/Resources/readme-artifacts/no-stable-release/buddy-works-alpha-1.3.0.zip differ diff --git a/tests/Resources/readme-artifacts/readme-in-second-dir/buddy-works-alpha-1.3.0.zip b/tests/Resources/readme-artifacts/readme-in-second-dir/buddy-works-alpha-1.3.0.zip new file mode 100644 index 00000000..5a3f81ec Binary files /dev/null and b/tests/Resources/readme-artifacts/readme-in-second-dir/buddy-works-alpha-1.3.0.zip differ diff --git a/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.2.0.zip b/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.2.0.zip new file mode 100644 index 00000000..dfa754b6 Binary files /dev/null and b/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.2.0.zip differ diff --git a/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.3.0.zip b/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.3.0.zip new file mode 100644 index 00000000..6c46ad9f Binary files /dev/null and b/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.3.0.zip differ diff --git a/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.4.0-beta.zip b/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.4.0-beta.zip new file mode 100644 index 00000000..16adfdad Binary files /dev/null and b/tests/Resources/readme-artifacts/readme/buddy-works-alpha-1.4.0-beta.zip differ diff --git a/tests/Resources/readme-artifacts/wrong-case-readme/buddy-works-alpha-1.3.0.zip b/tests/Resources/readme-artifacts/wrong-case-readme/buddy-works-alpha-1.3.0.zip new file mode 100644 index 00000000..405c54bf Binary files /dev/null and b/tests/Resources/readme-artifacts/wrong-case-readme/buddy-works-alpha-1.3.0.zip differ diff --git a/tests/Unit/Service/Organization/PackageDetailsParamConverterTest.php b/tests/Unit/Service/Organization/PackageDetailsParamConverterTest.php new file mode 100644 index 00000000..9e9e57fc --- /dev/null +++ b/tests/Unit/Service/Organization/PackageDetailsParamConverterTest.php @@ -0,0 +1,54 @@ +getMockBuilder(PackageQuery::class)->getMock()); + + $this->expectException(BadRequestHttpException::class); + $converter->apply(new Request(), new ParamConverter([])); + } + + public function testConvertPackage(): void + { + $package = new PackageDetails($id = '12cfc5f0-19d7-4144-916c-cfbbf9384c29', 'e20ea9cc-de3e-4d10-9e81-e30b6c3d217c'); + $queryMock = $this->createMock(PackageQuery::class); + $queryMock->expects(self::once())->method('getDetailsById')->willReturn(Option::some($package)); + + $converter = new PackageDetailsParamConverter($queryMock); + $converter->apply($request = new Request([], [], ['package' => $id]), new ParamConverter(['name' => 'package'])); + + self::assertEquals($package, $request->attributes->get('package')); + } + + public function testCheckIfPackageBelongsToOrganization(): void + { + $package = new PackageDetails($id = '12cfc5f0-19d7-4144-916c-cfbbf9384c29', 'e20ea9cc-de3e-4d10-9e81-e30b6c3d217c'); + $queryMock = $this->createMock(PackageQuery::class); + $queryMock->expects(self::once())->method('getDetailsById')->willReturn(Option::some($package)); + + $converter = new PackageDetailsParamConverter($queryMock); + + $this->expectException(NotFoundHttpException::class); + $converter->apply($request = new Request([], [], [ + 'package' => $id, + 'organization' => OrganizationMother::some(), + ]), new ParamConverter(['name' => 'package'])); + } +} diff --git a/tests/Unit/Service/PackageSynchronizer/ReadmeComposerPackageSynchronizerTest.php b/tests/Unit/Service/PackageSynchronizer/ReadmeComposerPackageSynchronizerTest.php new file mode 100644 index 00000000..997ee8d5 --- /dev/null +++ b/tests/Unit/Service/PackageSynchronizer/ReadmeComposerPackageSynchronizerTest.php @@ -0,0 +1,125 @@ +synchronizer = new ComposerPackageSynchronizer( + new PackageManager($fileStorage, $baseDir, new Filesystem()), + new PackageNormalizer(), + $this->createMock(PackageRepository::class), + $fileStorage, + 'gitlab.com' + ); + $this->resourcesDir = __DIR__.'/../../../Resources/'; + + $this->path = $baseDir.'/buddy/p/buddy-works/alpha.json'; + @unlink($this->path); + } + + protected function tearDown(): void + { + @unlink($this->path); + } + + public function testNoReadme(): void + { + $package = PackageMother::withOrganization('artifact', $this->resourcesDir.'readme-artifacts/no-readme', 'buddy'); + $this->synchronizer->synchronize($package); + + self::assertFileExists($this->path); + + $json = unserialize((string) file_get_contents($this->path)); + self::assertCount(1, $json['packages']['buddy-works/alpha']); + + self::assertCount(1, $package->versions()); + self::assertEquals('1.2.0', $package->latestReleasedVersion()); + self::assertNull($package->readme()); + } + + public function testReadme(): void + { + $package = PackageMother::withOrganization('artifact', $this->resourcesDir.'readme-artifacts/readme', 'buddy'); + $this->synchronizer->synchronize($package); + + self::assertFileExists($this->path); + + $json = unserialize((string) file_get_contents($this->path)); + self::assertCount(3, $json['packages']['buddy-works/alpha']); + + self::assertCount(3, $package->versions()); + self::assertEquals('1.3.0', $package->latestReleasedVersion()); + self::assertEquals("

Test

\n

Testing

\n", $package->readme()); + } + + public function testCaseInsensitiveReadme(): void + { + $package = PackageMother::withOrganization('artifact', $this->resourcesDir.'readme-artifacts/wrong-case-readme', 'buddy'); + $this->synchronizer->synchronize($package); + + self::assertFileExists($this->path); + + $json = unserialize((string) file_get_contents($this->path)); + self::assertCount(1, $json['packages']['buddy-works/alpha']); + + self::assertCount(1, $package->versions()); + self::assertEquals('1.3.0', $package->latestReleasedVersion()); + self::assertEquals("

Test

\n

Testing

\n", $package->readme()); + } + + /** + * Artifacts imported should have the files in the top level directory + * Copies from VCS have a top level directory of the project name & commit ID + * This will test loading the files from that second directory, but will use artifact + * to make the test simpler. + */ + public function testReadmeInSecondLevelDirectory(): void + { + $package = PackageMother::withOrganization('artifact', $this->resourcesDir.'readme-artifacts/readme-in-second-dir', 'buddy'); + $this->synchronizer->synchronize($package); + + self::assertFileExists($this->path); + + $json = unserialize((string) file_get_contents($this->path)); + self::assertCount(1, $json['packages']['buddy-works/alpha']); + + self::assertCount(1, $package->versions()); + self::assertEquals('1.3.0', $package->latestReleasedVersion()); + self::assertEquals("

Test

\n

Testing

\n", $package->readme()); + } + + public function testUnstableVersion(): void + { + $package = PackageMother::withOrganization('artifact', $this->resourcesDir.'readme-artifacts/no-stable-release', 'buddy'); + $this->synchronizer->synchronize($package); + + self::assertFileExists($this->path); + + $json = unserialize((string) file_get_contents($this->path)); + self::assertCount(1, $json['packages']['buddy-works/alpha']); + + self::assertCount(1, $package->versions()); + self::assertEquals('no stable release', $package->latestReleasedVersion()); + self::assertEquals("

Test

\n

Testing

\n", $package->readme()); + } +}