Forgejo: add custom driver for codeberg/forgejo repositories
parent
6308749a61
commit
f5ea243160
|
@ -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
|
||||
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
|
||||
|
||||
Defaults to `false`. If set to true all HTTPS URLs will be tried with HTTP
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -210,7 +210,7 @@ EOT
|
|||
}
|
||||
if ($input->getOption('global') && !$this->authConfigFile->exists()) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
@ -838,7 +838,7 @@ EOT
|
|||
}
|
||||
|
||||
// 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')) {
|
||||
$this->authConfigSource->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->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;
|
||||
|
|
|
@ -86,6 +86,8 @@ class Config
|
|||
'bearer' => [],
|
||||
'bump-after-update' => false,
|
||||
'allow-missing-requirements' => false,
|
||||
'forgejo-domains' => ['codeberg.org'],
|
||||
'forgejo-token' => [],
|
||||
];
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
|
@ -191,7 +193,7 @@ class Config
|
|||
// override defaults with given config
|
||||
if (!empty($config['config']) && is_array($config['config'])) {
|
||||
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->setSourceOfConfigValue($val, $key, $source);
|
||||
} elseif (in_array($key, ['allow-plugins'], true) && isset($this->config[$key]) && is_array($this->config[$key]) && is_array($val)) {
|
||||
|
|
|
@ -100,7 +100,7 @@ class JsonConfigSource implements ConfigSourceInterface
|
|||
{
|
||||
$authConfig = $this->authConfig;
|
||||
$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);
|
||||
if ($authConfig) {
|
||||
$config[$key][$host] = $val;
|
||||
|
@ -120,7 +120,7 @@ class JsonConfigSource implements ConfigSourceInterface
|
|||
{
|
||||
$authConfig = $this->authConfig;
|
||||
$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);
|
||||
if ($authConfig) {
|
||||
unset($config[$key][$host]);
|
||||
|
@ -262,7 +262,7 @@ class JsonConfigSource implements ConfigSourceInterface
|
|||
$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] === []) {
|
||||
$config['config'][$prop] = new \stdClass;
|
||||
}
|
||||
|
|
|
@ -119,6 +119,7 @@ abstract class BaseIO implements IOInterface
|
|||
$githubOauth = $config->get('github-oauth');
|
||||
$gitlabOauth = $config->get('gitlab-oauth');
|
||||
$gitlabToken = $config->get('gitlab-token');
|
||||
$forgejoToken = $config->get('forgejo-token');
|
||||
$httpBasic = $config->get('http-basic');
|
||||
$bearerToken = $config->get('bearer');
|
||||
|
||||
|
@ -163,6 +164,15 @@ abstract class BaseIO implements IOInterface
|
|||
$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
|
||||
foreach ($httpBasic as $domain => $cred) {
|
||||
$this->checkAndSetAuthentication($domain, $cred['username'], $cred['password']);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -75,6 +75,7 @@ abstract class VcsDriver implements VcsDriverInterface
|
|||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
|
|
|
@ -83,6 +83,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt
|
|||
'gitlab' => 'Composer\Repository\Vcs\GitLabDriver',
|
||||
'bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver',
|
||||
'git-bitbucket' => 'Composer\Repository\Vcs\GitBitbucketDriver',
|
||||
'forgejo' => 'Composer\Repository\Vcs\ForgejoDriver',
|
||||
'git' => 'Composer\Repository\Vcs\GitDriver',
|
||||
'hg' => 'Composer\Repository\Vcs\HgDriver',
|
||||
'perforce' => 'Composer\Repository\Vcs\PerforceDriver',
|
||||
|
|
|
@ -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']
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue