1
0
Fork 0
composer/src/Composer/Repository/Vcs/ForgejoDriver.php

346 lines
12 KiB
PHP

<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Repository\Vcs;
use Composer\Cache;
use Composer\Config;
use Composer\Downloader\TransportException;
use Composer\IO\IOInterface;
use Composer\Json\JsonFile;
use Composer\Pcre\Preg;
use Composer\Util\Forgejo;
use Composer\Util\ForgejoRepositoryData;
use Composer\Util\ForgejoUrl;
use Composer\Util\Http\Response;
class ForgejoDriver extends VcsDriver
{
/** @var ForgejoUrl */
private $forgejoUrl;
/** @var ForgejoRepositoryData */
private $repositoryData;
/** @var ?GitDriver */
protected $gitDriver = null;
/** @var array<int|string, string> Map of tag name to identifier */
private $tags;
/** @var array<int|string, string> Map of branch name to identifier */
private $branches;
public function initialize(): void
{
$this->forgejoUrl = ForgejoUrl::create($this->url);
$this->originUrl = $this->forgejoUrl->originUrl;
$this->cache = new Cache($this->io, $this->config->get('cache-repo-dir').'/'.$this->originUrl.'/'.$this->forgejoUrl->owner.'/'.$this->forgejoUrl->repository);
$this->cache->setReadOnly($this->config->get('cache-read-only'));
$this->fetchRepositoryData();
}
public function getFileContent(string $file, string $identifier): ?string
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getFileContent($file, $identifier);
}
$resource = $this->forgejoUrl->apiUrl.'/contents/' . $file . '?ref='.urlencode($identifier);
$resource = $this->getContents($resource)->decodeJson();
// The Forgejo contents API only returns files up to 1MB as base64 encoded files
// larger files either need be fetched with a raw accept header or by using the git blob endpoint
if ((!isset($resource['content']) || $resource['content'] === '') && $resource['encoding'] === 'none' && isset($resource['git_url'])) {
$resource = $this->getContents($resource['git_url'])->decodeJson();
}
if (!isset($resource['content']) || $resource['encoding'] !== 'base64' || false === ($content = base64_decode($resource['content'], true))) {
throw new \RuntimeException('Could not retrieve ' . $file . ' for '.$identifier);
}
return $content;
}
public function getChangeDate(string $identifier): ?\DateTimeImmutable
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getChangeDate($identifier);
}
$resource = $this->forgejoUrl->apiUrl.'/git/commits/'.urlencode($identifier).'?verification=false&files=false';
$commit = $this->getContents($resource)->decodeJson();
return new \DateTimeImmutable($commit['commit']['committer']['date']);
}
public function getRootIdentifier(): string
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getRootIdentifier();
}
return $this->repositoryData->defaultBranch;
}
public function getBranches(): array
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getBranches();
}
if (null === $this->branches) {
$branches = [];
$resource = $this->forgejoUrl->apiUrl.'/branches?per_page=100';
do {
$response = $this->getContents($resource);
$branchData = $response->decodeJson();
foreach ($branchData as $branch) {
$branches[$branch['name']] = $branch['commit']['id'];
}
$resource = $this->getNextPage($response);
} while ($resource);
$this->branches = $branches;
}
return $this->branches;
}
public function getTags(): array
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getTags();
}
if (null === $this->tags) {
$tags = [];
$resource = $this->forgejoUrl->apiUrl.'/tags?per_page=100';
do {
$response = $this->getContents($resource);
$tagsData = $response->decodeJson();
foreach ($tagsData as $tag) {
$tags[$tag['tag_name']] = $tag['commit']['sha'];
}
$resource = $this->getNextPage($response);
} while ($resource);
$this->tags = $tags;
}
return $this->tags;
}
public function getDist(string $identifier): ?array
{
$url = $this->forgejoUrl->apiUrl.'/archive/'.$identifier.'.zip';
return ['type' => 'zip', 'url' => $url, 'reference' => $identifier, 'shasum' => ''];
}
public function getComposerInformation(string $identifier): ?array
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getComposerInformation($identifier);
}
if (!isset($this->infoCache[$identifier])) {
if ($this->shouldCache($identifier) && false !== ($res = $this->cache->read($identifier))) {
$composer = JsonFile::parseJson($res);
} else {
$composer = $this->getBaseComposerInformation($identifier);
if ($this->shouldCache($identifier)) {
$this->cache->write($identifier, (string) json_encode($composer));
}
}
if ($composer !== null) {
// specials for forgejo
if (isset($composer['support']) && !is_array($composer['support'])) {
$composer['support'] = [];
}
if (!isset($composer['support']['source'])) {
if (false !== ($label = array_search($identifier, $this->getTags(), true))) {
$composer['support']['source'] = $this->repositoryData->htmlUrl.'/tag/' . $label;
} elseif (false !== ($label = array_search($identifier, $this->getBranches(), true))) {
$composer['support']['source'] = $this->repositoryData->htmlUrl.'/branch/'.$label;
} else {
$composer['support']['source'] = $this->repositoryData->htmlUrl.'/commit/'.$identifier;
}
}
if (!isset($composer['support']['issues']) && $this->repositoryData->hasIssues) {
$composer['support']['issues'] = $this->repositoryData->htmlUrl.'/issues';
}
if (!isset($composer['abandoned']) && $this->repositoryData->isArchived) {
$composer['abandoned'] = true;
}
}
$this->infoCache[$identifier] = $composer;
}
return $this->infoCache[$identifier];
}
public function getSource(string $identifier): array
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getSource($identifier);
}
if ($this->repositoryData->isPrivate) {
// Private Forgejo repositories should be accessed using the
// SSH version of the URL.
$url = $this->repositoryData->sshUrl;
} else {
$url = $this->getUrl();
}
return ['type' => 'git', 'url' => $url, 'reference' => $identifier];
}
public function getUrl(): string
{
if ($this->gitDriver !== null) {
return $this->gitDriver->getUrl();
}
return $this->repositoryData->isPrivate ? $this->repositoryData->sshUrl : $this->repositoryData->httpCloneUrl;
}
public static function supports(IOInterface $io, Config $config, string $url, bool $deep = false): bool
{
$forgejoUrl = ForgejoUrl::tryFrom($url);
if ($forgejoUrl === null) {
return false;
}
if (!in_array(strtolower($forgejoUrl->originUrl), $config->get('forgejo-domains'), true)) {
return false;
}
if (!extension_loaded('openssl')) {
$io->writeError('Skipping Forgejo driver for '.$url.' because the OpenSSL PHP extension is missing.', true, IOInterface::VERBOSE);
return false;
}
return true;
}
protected function setupGitDriver(string $url): void
{
$this->gitDriver = new GitDriver(
['url' => $url],
$this->io,
$this->config,
$this->httpDownloader,
$this->process
);
$this->gitDriver->initialize();
}
private function fetchRepositoryData(): void
{
if ($this->repositoryData !== null) {
return;
}
$data = $this->getContents($this->forgejoUrl->apiUrl, true)->decodeJson();
if (null === $data && null !== $this->gitDriver) {
return;
}
$this->repositoryData = ForgejoRepositoryData::fromRemoteData($data);
}
protected function getNextPage(Response $response): ?string
{
$header = $response->getHeader('link');
if ($header === null) {
return null;
}
$links = explode(',', $header);
foreach ($links as $link) {
if (Preg::isMatch('{<(.+?)>; *rel="next"}', $link, $match)) {
return $match[1];
}
}
return null;
}
protected function getContents(string $url, bool $fetchingRepoData = false): Response
{
$forgejo = new Forgejo($this->io, $this->config, $this->httpDownloader);
try {
return parent::getContents($url);
} catch (TransportException $e) {
switch ($e->getCode()) {
case 401:
case 403:
case 404:
case 429:
if (!$fetchingRepoData) {
throw $e;
}
if (!$this->io->isInteractive()) {
$this->attemptCloneFallback();
return new Response(['url' => 'dummy'], 200, [], 'null');
}
if (
!$this->io->hasAuthentication($this->originUrl) &&
$forgejo->authorizeOAuthInteractively($this->forgejoUrl->originUrl, $e->getCode() === 429 ? 'API limit exhausted. Enter your Forgejo credentials to get a larger API limit (<info>'.$this->url.'</info>)' : null)
) {
return parent::getContents($url);
}
throw $e;
default:
throw $e;
}
}
}
/**
* @phpstan-impure
*
* @return true
* @throws \RuntimeException
*/
protected function attemptCloneFallback(): bool
{
try {
// If this repository may be private (hard to say for sure,
// Forgejo returns 404 for private repositories) and we
// cannot ask for authentication credentials (because we
// are not interactive) then we fallback to GitDriver.
$this->setupGitDriver($this->forgejoUrl->generateSshUr());
return true;
} catch (\RuntimeException $e) {
$this->gitDriver = null;
$this->io->writeError('<error>Failed to clone the '.$this->forgejoUrl->generateSshUr().' repository, try running in interactive mode so that you can enter your Forgejo credentials</error>');
throw $e;
}
}
}