1
0
Fork 0

Forgejo: add custom driver for codeberg/forgejo repositories

pull/12307/head
Stephan Vock 2025-02-10 19:07:24 +00:00
parent 6308749a61
commit f5ea243160
No known key found for this signature in database
13 changed files with 764 additions and 6 deletions

View File

@ -246,6 +246,22 @@ private repositories which will later be cloned in GitLab CI jobs with a
using HTTP basic auth. By default, Composer will generate a git-over-SSH using HTTP basic auth. By default, Composer will generate a git-over-SSH
URL for private repositories and HTTP(S) only for public. URL for private repositories and HTTP(S) only for public.
## forgejo-domains
Defaults to `["codeberg.org"]`. A list of domains of Forgejo servers.
This is used if you use the `forgejo` repository type.
## forgejo-token
A list of domain names and username/access-tokens to authenticate against them. For
example using `{"codeberg.org": {"username": "forgejo-user", "token": "access-token"}}` as the
value of this option will let Composer authenticate against codeberg.org.
Please note: If the package is not hosted at
codeberg.org the domain names must be also specified with the
[`forgejo-domains`](06-config.md#forgejo-domains) option.
Further info can also be found [here](articles/authentication-for-private-packages.md#forgejo-token)
## disable-tls ## disable-tls
Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP

View File

@ -360,3 +360,41 @@ php composer.phar config [--global] --editor --auth
} }
} }
``` ```
## forgejo-token
> **Note:** For the forge authentication to work on private Forgejo instances, the
> [`forgejo-domains`](../06-config.md#forgejo-domains) section should also contain the URL.
To create a new access token, go to your [applications section on Forgejo](https://codeberg.org/user/settings/applications)
(or the equivalent URL on your private instance) and create a new access token. See also [the Forgejo access token documentation](https://docs.codeberg.org/advanced/access-token/) for more information.
When creating a Forgejo access token, make sure it has the `read:repository` scope.
### Command line gitlab-token
```shell
php composer.phar config [--global] forgejo-token.gitlab.example.org username access-token
```
In the above command, the config key `forgejo-token.forgejo.example.org` consists of two parts:
- `forgejo-token` is the authentication method.
- `forgejo.example.org` is the host name of your Forgejo instance, you should replace it with the host name of your Forgejo instance or use `codeberg.org` if you don't have a self-hosted Forgejo instance.
### Manual forgejo-token
```shell
php composer.phar config [--global] --editor --auth
```
```json
{
"forgejo-token": {
"forgejo.example.org": {
"username": "forgejo-user",
"token": "access-token"
}
}
}
```

View File

@ -210,7 +210,7 @@ EOT
} }
if ($input->getOption('global') && !$this->authConfigFile->exists()) { if ($input->getOption('global') && !$this->authConfigFile->exists()) {
touch($this->authConfigFile->getPath()); touch($this->authConfigFile->getPath());
$this->authConfigFile->write(['bitbucket-oauth' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject, 'gitlab-token' => new \ArrayObject, 'http-basic' => new \ArrayObject, 'bearer' => new \ArrayObject]); $this->authConfigFile->write(['bitbucket-oauth' => new \ArrayObject, 'github-oauth' => new \ArrayObject, 'gitlab-oauth' => new \ArrayObject, 'gitlab-token' => new \ArrayObject, 'http-basic' => new \ArrayObject, 'bearer' => new \ArrayObject, 'forgejo-token' => new \ArrayObject()]);
Silencer::call('chmod', $this->authConfigFile->getPath(), 0600); Silencer::call('chmod', $this->authConfigFile->getPath(), 0600);
} }
@ -838,7 +838,7 @@ EOT
} }
// handle auth // handle auth
if (Preg::isMatch('/^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|http-basic|bearer)\.(.+)/', $settingKey, $matches)) { if (Preg::isMatch('/^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|http-basic|bearer|forgejo-token)\.(.+)/', $settingKey, $matches)) {
if ($input->getOption('unset')) { if ($input->getOption('unset')) {
$this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->authConfigSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
@ -867,6 +867,12 @@ EOT
} }
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]); $this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], ['username' => $values[0], 'password' => $values[1]]); $this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], ['username' => $values[0], 'password' => $values[1]]);
} elseif ($matches[1] === 'forgejo-token') {
if (2 !== count($values)) {
throw new \RuntimeException('Expected two arguments (username, access token), got '.count($values));
}
$this->configSource->removeConfigSetting($matches[1].'.'.$matches[2]);
$this->authConfigSource->addConfigSetting($matches[1].'.'.$matches[2], ['username' => $values[0], 'token' => $values[1]]);
} }
return 0; return 0;

View File

@ -86,6 +86,8 @@ class Config
'bearer' => [], 'bearer' => [],
'bump-after-update' => false, 'bump-after-update' => false,
'allow-missing-requirements' => false, 'allow-missing-requirements' => false,
'forgejo-domains' => ['codeberg.org'],
'forgejo-token' => [],
]; ];
/** @var array<string, mixed> */ /** @var array<string, mixed> */
@ -191,7 +193,7 @@ class Config
// override defaults with given config // override defaults with given config
if (!empty($config['config']) && is_array($config['config'])) { if (!empty($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $val) { foreach ($config['config'] as $key => $val) {
if (in_array($key, ['bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer'], true) && isset($this->config[$key])) { if (in_array($key, ['bitbucket-oauth', 'github-oauth', 'gitlab-oauth', 'gitlab-token', 'http-basic', 'bearer', 'forgejo-token'], true) && isset($this->config[$key])) {
$this->config[$key] = array_merge($this->config[$key], $val); $this->config[$key] = array_merge($this->config[$key], $val);
$this->setSourceOfConfigValue($val, $key, $source); $this->setSourceOfConfigValue($val, $key, $source);
} elseif (in_array($key, ['allow-plugins'], true) && isset($this->config[$key]) && is_array($this->config[$key]) && is_array($val)) { } elseif (in_array($key, ['allow-plugins'], true) && isset($this->config[$key]) && is_array($this->config[$key]) && is_array($val)) {

View File

@ -100,7 +100,7 @@ class JsonConfigSource implements ConfigSourceInterface
{ {
$authConfig = $this->authConfig; $authConfig = $this->authConfig;
$this->manipulateJson('addConfigSetting', static function (&$config, $key, $val) use ($authConfig): void { $this->manipulateJson('addConfigSetting', static function (&$config, $key, $val) use ($authConfig): void {
if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|platform)\.}', $key)) { if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|forgejo-token|platform)\.}', $key)) {
[$key, $host] = explode('.', $key, 2); [$key, $host] = explode('.', $key, 2);
if ($authConfig) { if ($authConfig) {
$config[$key][$host] = $val; $config[$key][$host] = $val;
@ -120,7 +120,7 @@ class JsonConfigSource implements ConfigSourceInterface
{ {
$authConfig = $this->authConfig; $authConfig = $this->authConfig;
$this->manipulateJson('removeConfigSetting', static function (&$config, $key) use ($authConfig): void { $this->manipulateJson('removeConfigSetting', static function (&$config, $key) use ($authConfig): void {
if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|platform)\.}', $key)) { if (Preg::isMatch('{^(bitbucket-oauth|github-oauth|gitlab-oauth|gitlab-token|bearer|http-basic|forgejo-token|platform)\.}', $key)) {
[$key, $host] = explode('.', $key, 2); [$key, $host] = explode('.', $key, 2);
if ($authConfig) { if ($authConfig) {
unset($config[$key][$host]); unset($config[$key][$host]);
@ -262,7 +262,7 @@ class JsonConfigSource implements ConfigSourceInterface
$config['autoload-dev'][$prop] = new \stdClass; $config['autoload-dev'][$prop] = new \stdClass;
} }
} }
foreach (['platform', 'http-basic', 'bearer', 'gitlab-token', 'gitlab-oauth', 'github-oauth', 'preferred-install'] as $prop) { foreach (['platform', 'http-basic', 'bearer', 'gitlab-token', 'gitlab-oauth', 'github-oauth', 'forgejo-token', 'preferred-install'] as $prop) {
if (isset($config['config'][$prop]) && $config['config'][$prop] === []) { if (isset($config['config'][$prop]) && $config['config'][$prop] === []) {
$config['config'][$prop] = new \stdClass; $config['config'][$prop] = new \stdClass;
} }

View File

@ -119,6 +119,7 @@ abstract class BaseIO implements IOInterface
$githubOauth = $config->get('github-oauth'); $githubOauth = $config->get('github-oauth');
$gitlabOauth = $config->get('gitlab-oauth'); $gitlabOauth = $config->get('gitlab-oauth');
$gitlabToken = $config->get('gitlab-token'); $gitlabToken = $config->get('gitlab-token');
$forgejoToken = $config->get('forgejo-token');
$httpBasic = $config->get('http-basic'); $httpBasic = $config->get('http-basic');
$bearerToken = $config->get('bearer'); $bearerToken = $config->get('bearer');
@ -163,6 +164,15 @@ abstract class BaseIO implements IOInterface
$this->checkAndSetAuthentication($domain, $username, $password); $this->checkAndSetAuthentication($domain, $username, $password);
} }
foreach ($forgejoToken as $domain => $cred) {
if (!in_array($domain, $config->get('forgejo-domains'), true)) {
$this->debug($domain.' is not in the configured forgejo-domains, adding it implicitly as authentication is configured for this domain');
$config->merge(['config' => ['forgejo-domains' => array_merge($config->get('forgejo-domains'), [$domain])]], 'implicit-due-to-auth');
}
$this->checkAndSetAuthentication($domain, $cred['username'], $cred['token']);
}
// reload http basic credentials from config if available // reload http basic credentials from config if available
foreach ($httpBasic as $domain => $cred) { foreach ($httpBasic as $domain => $cred) {
$this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']); $this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']);

View File

@ -0,0 +1,321 @@
<?php declare(strict_types=1);
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\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
{
try {
return parent::getContents($url);
} catch (TransportException $e) {
switch ($e->getCode()) {
case 404:
// try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404
if (!$fetchingRepoData) {
throw $e;
}
if (!$this->io->isInteractive()) {
$this->attemptCloneFallback();
return new Response(['url' => 'dummy'], 200, [], 'null');
}
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;
}
}
}

View File

@ -75,6 +75,7 @@ abstract class VcsDriver implements VcsDriverInterface
/** /**
* Returns whether or not the given $identifier should be cached or not. * Returns whether or not the given $identifier should be cached or not.
* @phpstan-assert-if-true !null $this->cache
*/ */
protected function shouldCache(string $identifier): bool protected function shouldCache(string $identifier): bool
{ {

View File

@ -83,6 +83,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
'gitlab' => 'Composer\Repository\Vcs\GitLabDriver', 'gitlab' => 'Composer\Repository\Vcs\GitLabDriver',
'bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', 'bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver',
'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver', 'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver',
'forgejo' => 'Composer\Repository\Vcs\ForgejoDriver',
'git' => 'Composer\Repository\Vcs\GitDriver', 'git' => 'Composer\Repository\Vcs\GitDriver',
'hg' => 'Composer\Repository\Vcs\HgDriver', 'hg' => 'Composer\Repository\Vcs\HgDriver',
'perforce' => 'Composer\Repository\Vcs\PerforceDriver', 'perforce' => 'Composer\Repository\Vcs\PerforceDriver',

View File

@ -0,0 +1,59 @@
<?php declare(strict_types=1);
namespace Composer\Util;
/**
* @internal
* @readonly
*/
final class ForgejoRepositoryData
{
/** @var string */
public $htmlUrl;
/** @var string */
public $sshUrl;
/** @var string */
public $httpCloneUrl;
/** @var bool */
public $isPrivate;
/** @var string */
public $defaultBranch;
/** @var bool */
public $hasIssues;
/** @var bool */
public $isArchived;
public function __construct(
string $htmlUrl,
string $httpCloneUrl,
string $sshUrl,
bool $isPrivate,
string $defaultBranch,
bool $hasIssues,
bool $isArchived
) {
$this->htmlUrl = $htmlUrl;
$this->httpCloneUrl = $httpCloneUrl;
$this->sshUrl = $sshUrl;
$this->isPrivate = $isPrivate;
$this->defaultBranch = $defaultBranch;
$this->hasIssues = $hasIssues;
$this->isArchived = $isArchived;
}
/**
* @param array<string, mixed> $data
*/
public static function fromRemoteData(array $data): self
{
return new self(
$data['html_url'],
$data['clone_url'],
$data['ssh_url'],
$data['private'],
$data['default_branch'],
$data['has_issues'],
$data['archived']
);
}
}

View File

@ -0,0 +1,67 @@
<?php declare(strict_types=1);
namespace Composer\Util;
use Composer\Pcre\Preg;
/**
* @internal
* @readonly
*/
final class ForgejoUrl
{
public const URL_REGEX = '{^(?:(?:https?|git)://([^/]+)/|git@([^:]+):/?)([^/]+)/([^/]+?)(?:\.git|/)?$}';
/** @var string */
public $owner;
/** @var string */
public $repository;
/** @var string */
public $originUrl;
/** @var string */
public $apiUrl;
private function __construct(
string $owner,
string $repository,
string $originUrl,
string $apiUrl
) {
$this->owner = $owner;
$this->repository = $repository;
$this->originUrl = $originUrl;
$this->apiUrl = $apiUrl;
}
public static function create(string $repoUrl): self
{
$url = self::tryFrom($repoUrl);
if ($url !== null) {
return $url;
}
throw new \InvalidArgumentException('This is not a valid Forgejo URL: ' . $repoUrl);
}
public static function tryFrom(?string $repoUrl): ?self
{
if ($repoUrl === null || ! Preg::isMatch(self::URL_REGEX, $repoUrl, $match)) {
return null;
}
$originUrl = strtolower($match[1] ?? (string) $match[2]);
$apiBase = $originUrl . '/api/v1';
return new self(
$match[3],
$match[4],
$originUrl,
sprintf('https://%s/repos/%s/%s', $apiBase, $match[3], $match[4])
);
}
public function generateSshUr(): string
{
return 'git@' . $this->originUrl . ':'.$this->owner.'/'.$this->repository.'.git';
}
}

View File

@ -0,0 +1,191 @@
<?php declare(strict_types=1);
namespace Composer\Test\Repository\Vcs;
use Composer\Config;
use Composer\IO\IOInterface;
use Composer\Repository\Vcs\ForgejoDriver;
use Composer\Test\Mock\HttpDownloaderMock;
use Composer\Test\TestCase;
use Composer\Util\Filesystem;
use PHPUnit\Framework\MockObject\MockObject;
class ForgejoDriverTest extends TestCase
{
/** @var string */
private $home;
/** @var Config */
private $config;
/** @var IOInterface&MockObject */
private $io;
/** @var HttpDownloaderMock */
private $httpDownloader;
public function setUp(): void
{
$this->home = self::getUniqueTmpDirectory();
$this->config = new Config();
$this->config->merge([
'config' => [
'home' => $this->home,
'forgejo-domains' => ['codeberg.org'],
],
]);
$this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$this->httpDownloader = $this->getHttpDownloaderMock($this->io, $this->config);
}
protected function tearDown(): void
{
parent::tearDown();
$fs = new Filesystem;
$fs->removeDirectory($this->home);
}
public function testPublicRepository(): void
{
$this->expectInteractiveIO();
$this->httpDownloader->expects(
[
['url' => 'https://codeberg.org/api/v1/repos/acme/repo', 'body' => (string) json_encode([
'default_branch' => 'main',
'has_issues' => true,
'archived' => false,
'private' => false,
'html_url' => 'https://codeberg.org/acme/repo',
'ssh_url' => 'git@codeberg.org:acme/repo.git',
'clone_url' => 'https://codeberg.org/acme/repo.git'
])],
],
true
);
$driver = $this->initializeDriver('https://codeberg.org/acme/repo.git');
self::assertEquals('main', $driver->getRootIdentifier());
$sha = 'SOMESHA';
$dist = $driver->getDist($sha);
self::assertIsArray($dist);
self::assertEquals('zip', $dist['type']);
self::assertEquals('https://codeberg.org/api/v1/repos/acme/repo/archive/SOMESHA.zip', $dist['url']);
self::assertEquals($sha, $dist['reference']);
$source = $driver->getSource($sha);
self::assertEquals('git', $source['type']);
self::assertEquals('https://codeberg.org/acme/repo.git', $source['url']);
self::assertEquals($sha, $source['reference']);
}
public function testGetBranches(): void
{
$this->expectInteractiveIO();
$this->httpDownloader->expects(
[
['url' => 'https://codeberg.org/api/v1/repos/acme/repo', 'body' => (string) json_encode([
'default_branch' => 'main',
'has_issues' => true,
'archived' => false,
'private' => false,
'html_url' => 'https://codeberg.org/acme/repo',
'ssh_url' => 'git@codeberg.org:acme/repo.git',
'clone_url' => 'https://codeberg.org/acme/repo.git'
])],
['url' => 'https://codeberg.org/api/v1/repos/acme/repo/branches?per_page=100', 'body' => (string) json_encode([
['name' => 'main', 'commit' => ['id' => 'SOMESHA']],
])]
],
true
);
$driver = $this->initializeDriver('https://codeberg.org/acme/repo.git');
self::assertEquals(['main' => 'SOMESHA'], $driver->getBranches());
}
public function testGetTags(): void
{
$this->expectInteractiveIO();
$this->httpDownloader->expects(
[
['url' => 'https://codeberg.org/api/v1/repos/acme/repo', 'body' => (string) json_encode([
'default_branch' => 'main',
'has_issues' => true,
'archived' => false,
'private' => false,
'html_url' => 'https://codeberg.org/acme/repo',
'ssh_url' => 'git@codeberg.org:acme/repo.git',
'clone_url' => 'https://codeberg.org/acme/repo.git'
])],
['url' => 'https://codeberg.org/api/v1/repos/acme/repo/tags?per_page=100', 'body' => (string) json_encode([
['tag_name' => '1.0', 'commit' => ['sha' => 'SOMESHA']]
])]
],
true
);
$driver = $this->initializeDriver('https://codeberg.org/acme/repo.git');
self::assertEquals(['1.0' => 'SOMESHA'], $driver->getTags());
}
public function testGetEmptyFileContent(): void
{
$this->expectInteractiveIO();
$this->httpDownloader->expects(
[
['url' => 'https://codeberg.org/api/v1/repos/acme/repo', 'body' => (string) json_encode([
'default_branch' => 'main',
'has_issues' => true,
'archived' => false,
'private' => false,
'html_url' => 'https://codeberg.org/acme/repo',
'ssh_url' => 'git@codeberg.org:acme/repo.git',
'clone_url' => 'https://codeberg.org/acme/repo.git'
])],
['url' => 'https://codeberg.org/api/v1/repos/acme/repo/contents/composer.json?ref=main', 'body' => '{"encoding":"base64","content":""}']
],
true
);
$driver = $this->initializeDriver('https://codeberg.org/acme/repo.git');
self::assertSame('', $driver->getFileContent('composer.json', 'main'));
}
/**
* @dataProvider supportsProvider
*/
public function testSupports(bool $expected, string $repoUrl): void
{
self::assertSame($expected, ForgejoDriver::supports($this->io, $this->config, $repoUrl));
}
/**
* @return list<array{bool, string}>
*/
public static function supportsProvider(): array
{
return [
[false, 'https://example.org/acme/repo'],
[true, 'https://codeberg.org/acme/repository'],
];
}
private function initializeDriver(string $repoUrl): ForgejoDriver
{
$driver = new ForgejoDriver(['url' => $repoUrl], $this->io, $this->config, $this->httpDownloader, $this->getProcessExecutorMock());
$driver->initialize();
return $driver;
}
private function expectInteractiveIO(bool $isInteractive = true): void
{
$this->io->expects($this->any())
->method('isInteractive')
->will($this->returnValue($isInteractive));
}
}

View File

@ -0,0 +1,46 @@
<?php declare(strict_types=1);
namespace Composer\Test\Util;
use Composer\Test\TestCase;
use Composer\Util\ForgejoUrl;
class ForgejoUrlTest extends TestCase
{
/**
* @dataProvider createProvider
*/
public function testCreate(?string $repoUrl): void
{
$forgejoUrl = ForgejoUrl::tryFrom($repoUrl);
$this->assertNotNull($forgejoUrl);
$this->assertSame('codeberg.org', $forgejoUrl->originUrl);
$this->assertSame('acme', $forgejoUrl->owner);
$this->assertSame('repo', $forgejoUrl->repository);
$this->assertSame('https://codeberg.org/api/v1/repos/acme/repo', $forgejoUrl->apiUrl);
}
public static function createProvider(): array
{
return [
['git@codeberg.org:acme/repo.git'],
['https://codeberg.org/acme/repo'],
['https://codeberg.org/acme/repo.git'],
];
}
public function testCreateInvalid(): void
{
$this->expectException(\InvalidArgumentException::class);
ForgejoUrl::create('https://example.org');
}
public function testGenerateSshUrl(): void
{
$forgejoUrl = ForgejoUrl::create('git@codeberg.org:acme/repo.git');
$this->assertSame('git@codeberg.org:acme/repo.git', $forgejoUrl->generateSshUr());
}
}