repman-io/repman

View on GitHub
src/Service/PackageSynchronizer/ComposerPackageSynchronizer.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare(strict_types=1);

namespace Buddy\Repman\Service\PackageSynchronizer;

use Buddy\Repman\Entity\Organization\Package;
use Buddy\Repman\Entity\Organization\Package\Abandoned;
use Buddy\Repman\Entity\Organization\Package\Link;
use Buddy\Repman\Entity\Organization\Package\Version;
use Buddy\Repman\Repository\PackageRepository;
use Buddy\Repman\Service\Dist;
use Buddy\Repman\Service\Dist\Storage;
use Buddy\Repman\Service\Organization\PackageManager;
use Buddy\Repman\Service\PackageNormalizer;
use Buddy\Repman\Service\PackageSynchronizer;
use Buddy\Repman\Service\ReadmeExtractor;
use Buddy\Repman\Service\User\UserOAuthTokenRefresher;
use Composer\Config;
use Composer\Factory;
use Composer\IO\BufferIO;
use Composer\IO\IOInterface;
use Composer\Package\CompletePackage;
use Composer\Package\Link as ComposerLink;
use Composer\Package\PackageInterface;
use Composer\Repository\RepositoryFactory;
use Composer\Repository\RepositoryInterface;
use Composer\Semver\Comparator;
use Ramsey\Uuid\Uuid;
use Symfony\Component\Console\Output\OutputInterface;

final class ComposerPackageSynchronizer implements PackageSynchronizer
{
    private PackageManager $packageManager;
    private PackageNormalizer $packageNormalizer;
    private PackageRepository $packageRepository;
    private Storage $distStorage;
    private ReadmeExtractor $readmeExtractor;
    private UserOAuthTokenRefresher $tokenRefresher;
    private string $gitlabUrl;

    public function __construct(
        PackageManager $packageManager,
        PackageNormalizer $packageNormalizer,
        PackageRepository $packageRepository,
        Storage $distStorage,
        UserOAuthTokenRefresher $tokenRefresher,
        string $gitlabUrl
    ) {
        $this->packageManager = $packageManager;
        $this->packageNormalizer = $packageNormalizer;
        $this->packageRepository = $packageRepository;
        $this->distStorage = $distStorage;
        $this->tokenRefresher = $tokenRefresher;
        $this->gitlabUrl = $gitlabUrl;
        $this->readmeExtractor = new ReadmeExtractor($this->distStorage);
    }

    public function synchronize(Package $package): void
    {
        try {
            $io = $this->createIO($package);
            /** @var RepositoryInterface $repository */
            $repository = current(RepositoryFactory::defaultRepos($io, $this->createConfig($package, $io)));
            $json = ['packages' => []];
            $packages = $repository->getPackages();

            usort($packages, static function (PackageInterface $a, PackageInterface $b): int {
                if ($a->getVersion() === $b->getVersion()) {
                    return $a->getReleaseDate() <=> $b->getReleaseDate();
                }

                if ($a->getStability() === Version::STABILITY_STABLE && $b->getStability() !== Version::STABILITY_STABLE) {
                    return -1;
                }

                if ($a->getStability() !== Version::STABILITY_STABLE && $b->getStability() === Version::STABILITY_STABLE) {
                    return 1;
                }

                return Comparator::greaterThan($a->getVersion(), $b->getVersion()) ? 1 : -1;
            });

            if ($packages === []) {
                throw new \RuntimeException('Package not found');
            }

            $latest = current($packages);

            foreach ($packages as $p) {
                $json['packages'][$p->getPrettyName()][$p->getPrettyVersion()] = $this->packageNormalizer->normalize($p);
                if (Comparator::greaterThan($p->getVersion(), $latest->getVersion()) && $p->getStability() === Version::STABILITY_STABLE) {
                    $latest = $p;
                }
            }

            /** @var string|null $name */
            $name = $latest->getPrettyName();

            if ($name === null) {
                throw new \RuntimeException('Missing package name in latest version. Revision: '.$latest->getDistReference());
            }

            if (preg_match(Package::NAME_PATTERN, $name, $matches) !== 1) {
                throw new \RuntimeException("Package name {$name} is invalid");
            }

            if (!$package->isSynchronized() && $this->packageRepository->packageExist($name, $package->organizationId())) {
                throw new \RuntimeException("Package {$name} already exists. Package name must be unique within organization.");
            }

            $versions = [];
            foreach ($packages as $p) {
                if ($p->getDistUrl() !== null) {
                    $versions[] = [
                        'organizationAlias' => $package->organizationAlias(),
                        'packageName' => $p->getPrettyName(),
                        'prettyVersion' => $p->getPrettyVersion(),
                        'version' => $p->getVersion(),
                        'ref' => $p->getDistReference() ?? $p->getDistSha1Checksum(),
                        'distType' => $p->getDistType(),
                        'distUrl' => $p->getDistUrl(),
                        'authHeaders' => $this->getAuthHeaders($package),
                        'releaseDate' => \DateTimeImmutable::createFromMutable($p->getReleaseDate() ?? new \DateTime()),
                        'stability' => $p->getStability(),
                    ];
                }
            }

            usort($versions, fn ($item1, $item2) => $item2['releaseDate'] <=> $item1['releaseDate']);

            $encounteredVersions = [];
            $encounteredLinks = [];
            foreach ($versions as $version) {
                $dist = new Dist(
                    $version['organizationAlias'],
                    $version['packageName'],
                    $version['version'],
                    $version['ref'],
                    $version['distType']
                );

                if (
                    $latest->getVersion() !== $version['version']
                    && $package->keepLastReleases() > 0
                    && count($encounteredVersions) >= $package->keepLastReleases()
                ) {
                    $this->distStorage->remove($dist);
                    $package->removeVersion(new Version(
                        Uuid::uuid4(),
                        $version['prettyVersion'],
                        $version['ref'],
                        0,
                        $version['releaseDate'],
                        $version['stability']
                    ));

                    continue;
                }

                $this->distStorage->download(
                    $version['distUrl'],
                    $dist,
                    $this->getAuthHeaders($package)
                );

                if ($latest->getVersion() === $version['version']) {
                    $this->readmeExtractor->extractReadme($package, $dist);

                    // Set the version links
                    foreach (['requires', 'devRequires', 'provides', 'replaces', 'conflicts'] as $type) {
                        $functionName = 'get'.$type;
                        if (method_exists($latest, $functionName)) {
                            /** @var ComposerLink $link */
                            foreach ($latest->{$functionName}() as $link) {
                                $package->addLink(new Link(Uuid::uuid4(), $package, $type, $link->getTarget(), $link->getPrettyConstraint()));
                                $encounteredLinks[$type.'-'.$link->getTarget()] = true;
                            }
                        }
                    }

                    // suggests are different
                    foreach ($latest->getSuggests() as $linkName => $linkDescription) {
                        $package->addLink(new Link(Uuid::uuid4(), $package, 'suggests', $linkName, $linkDescription));
                        $encounteredLinks['suggests-'.$linkName] = true;
                    }

                    // Is abandoned ?
                    if ($latest instanceof CompletePackage && $latest->isAbandoned()) {
                        $package->setReplacementPackage($latest->getReplacementPackage() ?? '');
                    } else {
                        $package->setReplacementPackage(null);
                    }
                }

                $package->addOrUpdateVersion(
                    new Version(
                        Uuid::uuid4(),
                        $version['prettyVersion'],
                        $version['ref'],
                        $this->distStorage->size($dist),
                        $version['releaseDate'],
                        $version['stability']
                    )
                );

                $encounteredVersions[$version['prettyVersion']] = true;
            }

            $package->syncSuccess(
                $name,
                $latest instanceof CompletePackage ? ($latest->getDescription() ?? 'n/a') : 'n/a',
                $latest->getStability() === Version::STABILITY_STABLE ? $latest->getPrettyVersion() : 'no stable release',
                $encounteredVersions,
                $encounteredLinks,
                \DateTimeImmutable::createFromMutable($latest->getReleaseDate() ?? new \DateTime()),
            );

            $this->packageManager->saveProvider($json, $package->organizationAlias(), $name);
        } catch (\Throwable $exception) {
            $package->syncFailure(sprintf('Error: %s%s',
                $exception->getMessage(),
                isset($io) && strlen($io->getOutput()) > 1 ? "\nLogs:\n".$io->getOutput() : ''
            ));
        }
    }

    /**
     * @return string[]
     */
    private function getAuthHeaders(Package $package): array
    {
        if (!$package->hasOAuthToken()) {
            return [];
        }

        return [sprintf('Authorization: Bearer %s', $this->accessToken($package))];
    }

    private function getGitlabUrl(): string
    {
        $port = (string) \parse_url($this->gitlabUrl, \PHP_URL_PORT);
        if ($port !== '') {
            $port = ':'.$port;
        }

        return \parse_url($this->gitlabUrl, \PHP_URL_HOST).$port;
    }

    private function createIO(Package $package): BufferIO
    {
        $io = new BufferIO('', OutputInterface::VERBOSITY_VERY_VERBOSE);

        if ($package->type() === 'github-oauth') {
            $io->setAuthentication('github.com', $this->accessToken($package), 'x-oauth-basic');
        }

        if ($package->type() === 'gitlab-oauth') {
            $io->setAuthentication($this->getGitlabUrl(), $this->accessToken($package), 'oauth2');
        }

        if ($package->type() === 'bitbucket-oauth') {
            $io->setAuthentication('bitbucket.org', 'x-token-auth', $this->accessToken($package));
        }

        return $io;
    }

    private function accessToken(Package $package): string
    {
        return $package->oauthToken()->accessToken($this->tokenRefresher);
    }

    private function createConfig(Package $package, IOInterface $io): Config
    {
        unset(Config::$defaultRepositories['packagist.org']);
        $config = Factory::createConfig($io);
        $config->merge([
            'repositories' => [
                [
                    'type' => strpos($package->type(), '-oauth') !== false ? 'vcs' : $package->type(),
                    'url' => $package->repositoryUrl(),
                ],
            ],
        ]);
        if ($package->type() === 'gitlab-oauth') {
            $config->merge([
                'config' => [
                    'gitlab-domains' => [$this->getGitlabUrl()],
                ],
            ]);
        }

        return $config;
    }
}