commit
4002cab25b
|
@ -22,6 +22,7 @@ $vendorPath = 'vendor';
|
|||
$rm = new Repository\RepositoryManager();
|
||||
$rm->setLocalRepository(new Repository\FilesystemRepository(new JsonFile($vendorPath.'/.composer/installed.json')));
|
||||
$rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository');
|
||||
$rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository');
|
||||
$rm->setRepositoryClass('pear', 'Composer\Repository\PearRepository');
|
||||
$rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository');
|
||||
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
use Composer\Package\MemoryPackage;
|
||||
use Composer\Package\BasePackage;
|
||||
use Composer\Package\Link;
|
||||
use Composer\Package\LinkConstraint\VersionConstraint;
|
||||
use Composer\Package\Loader\ArrayLoader;
|
||||
use Composer\Json\JsonFile;
|
||||
|
||||
/**
|
||||
* FIXME This is majorly broken and incomplete, it was an experiment
|
||||
*
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class GitRepository extends ArrayRepository
|
||||
{
|
||||
protected $url;
|
||||
|
||||
public function __construct(array $url)
|
||||
{
|
||||
if (!filter_var($config['url'], FILTER_VALIDATE_URL)) {
|
||||
throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$url);
|
||||
}
|
||||
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
if (preg_match('#^(?:https?|git(?:\+ssh)?|ssh)://#', $this->url)) {
|
||||
// check if the repo is on github.com, read the composer.json file & branch/tags out of it
|
||||
// otherwise, maybe we have to clone the repo to figure out what's in it
|
||||
throw new \Exception('Not implemented yet');
|
||||
} elseif (file_exists($this->url)) {
|
||||
if (!file_exists($this->url.'/composer.json')) {
|
||||
throw new \InvalidArgumentException('The repository at url '.$this->url.' does not contain a composer.json file.');
|
||||
}
|
||||
$json = new JsonFile($this->url.'/composer.json');
|
||||
$config = $json->read();
|
||||
if (!$config) {
|
||||
throw new \UnexpectedValueException('Config file could not be parsed: '.$this->url.'/composer.json. Probably a JSON syntax error.');
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Could not find repository at url '.$this->url);
|
||||
}
|
||||
|
||||
$loader = new ArrayLoader($this->repositoryManager);
|
||||
$this->addPackage($loader->load($config));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
<?php
|
||||
|
||||
namespace Composer\Repository\Vcs;
|
||||
|
||||
use Composer\Json\JsonFile;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class GitDriver implements VcsDriverInterface
|
||||
{
|
||||
protected $url;
|
||||
protected $tags;
|
||||
protected $branches;
|
||||
protected $rootIdentifier;
|
||||
protected $infoCache = array();
|
||||
|
||||
public function __construct($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
$this->tmpDir = sys_get_temp_dir() . '/composer-' . preg_replace('{[^a-z0-9]}i', '-', $url) . '/';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
$url = escapeshellarg($this->url);
|
||||
$tmpDir = escapeshellarg($this->tmpDir);
|
||||
if (is_dir($this->tmpDir)) {
|
||||
exec(sprintf('cd %s && git fetch origin', $tmpDir), $output);
|
||||
} else {
|
||||
exec(sprintf('git clone %s %s', $url, $tmpDir), $output);
|
||||
}
|
||||
|
||||
$this->getTags();
|
||||
$this->getBranches();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRootIdentifier()
|
||||
{
|
||||
if (null === $this->rootIdentifier) {
|
||||
$this->rootIdentifier = 'master';
|
||||
exec(sprintf('cd %s && git branch --no-color -r', escapeshellarg($this->tmpDir)), $output);
|
||||
foreach ($output as $key => $branch) {
|
||||
if ($branch && preg_match('{/HEAD +-> +[^/]+/(\S+)}', $branch, $match)) {
|
||||
$this->rootIdentifier = $match[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->rootIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSource($identifier)
|
||||
{
|
||||
$label = array_search($identifier, (array) $this->tags) ?: $identifier;
|
||||
|
||||
return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getDist($identifier)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getComposerInformation($identifier)
|
||||
{
|
||||
if (!isset($this->infoCache[$identifier])) {
|
||||
exec(sprintf('cd %s && git show %s:composer.json', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output);
|
||||
$composer = implode("\n", $output);
|
||||
unset($output);
|
||||
|
||||
if (!$composer) {
|
||||
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl());
|
||||
}
|
||||
|
||||
$composer = JsonFile::parseJson($composer);
|
||||
|
||||
if (!isset($composer['time'])) {
|
||||
exec(sprintf('cd %s && git log -1 --format=\'%%at\' %s', escapeshellarg($this->tmpDir), escapeshellarg($identifier)), $output);
|
||||
$date = new \DateTime('@'.$output[0]);
|
||||
$composer['time'] = $date->format('Y-m-d H:i:s');
|
||||
}
|
||||
$this->infoCache[$identifier] = $composer;
|
||||
}
|
||||
|
||||
return $this->infoCache[$identifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
if (null === $this->tags) {
|
||||
exec(sprintf('cd %s && git tag', escapeshellarg($this->tmpDir)), $output);
|
||||
$this->tags = array_combine($output, $output);
|
||||
}
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getBranches()
|
||||
{
|
||||
if (null === $this->branches) {
|
||||
$branches = array();
|
||||
|
||||
exec(sprintf('cd %s && git branch --no-color -rv', escapeshellarg($this->tmpDir)), $output);
|
||||
foreach ($output as $key => $branch) {
|
||||
if ($branch && !preg_match('{^ *[^/]+/HEAD }', $branch)) {
|
||||
preg_match('{^ *[^/]+/(\S+) *([a-f0-9]+) .*$}', $branch, $match);
|
||||
$branches[$match[1]] = $match[2];
|
||||
}
|
||||
}
|
||||
|
||||
$this->branches = $branches;
|
||||
}
|
||||
|
||||
return $this->branches;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function hasComposerFile($identifier)
|
||||
{
|
||||
try {
|
||||
$this->getComposerInformation($identifier);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public static function supports($url, $deep = false)
|
||||
{
|
||||
if (preg_match('#(^git://|\.git$|git@|//git\.)#i', $url)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$deep) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO try to connect to the server
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
<?php
|
||||
|
||||
namespace Composer\Repository\Vcs;
|
||||
|
||||
use Composer\Json\JsonFile;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class GitHubDriver implements VcsDriverInterface
|
||||
{
|
||||
protected $owner;
|
||||
protected $repository;
|
||||
protected $tags;
|
||||
protected $branches;
|
||||
protected $rootIdentifier;
|
||||
protected $infoCache = array();
|
||||
|
||||
public function __construct($url)
|
||||
{
|
||||
preg_match('#^(?:https?|git)://github\.com/([^/]+)/(.+?)(?:\.git)?$#', $url, $match);
|
||||
$this->owner = $match[1];
|
||||
$this->repository = $match[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getRootIdentifier()
|
||||
{
|
||||
if (null === $this->rootIdentifier) {
|
||||
$repoData = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository), true);
|
||||
$this->rootIdentifier = $repoData['master_branch'] ?: 'master';
|
||||
}
|
||||
|
||||
return $this->rootIdentifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getUrl()
|
||||
{
|
||||
return 'http://github.com/'.$this->owner.'/'.$this->repository.'.git';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getSource($identifier)
|
||||
{
|
||||
$label = array_search($identifier, $this->getTags()) ?: $identifier;
|
||||
|
||||
return array('type' => 'git', 'url' => $this->getUrl(), 'reference' => $label);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getDist($identifier)
|
||||
{
|
||||
$label = array_search($identifier, $this->getTags()) ?: $identifier;
|
||||
$url = 'http://github.com/'.$this->owner.'/'.$this->repository.'/zipball/'.$label;
|
||||
|
||||
return array('type' => 'zip', 'url' => $url, 'reference' => $label, 'shasum' => '');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getComposerInformation($identifier)
|
||||
{
|
||||
if (!isset($this->infoCache[$identifier])) {
|
||||
$composer = @file_get_contents('https://raw.github.com/'.$this->owner.'/'.$this->repository.'/'.$identifier.'/composer.json');
|
||||
if (!$composer) {
|
||||
throw new \UnexpectedValueException('Failed to retrieve composer information for identifier '.$identifier.' in '.$this->getUrl());
|
||||
}
|
||||
|
||||
$composer = JsonFile::parseJson($composer);
|
||||
|
||||
if (!isset($composer['time'])) {
|
||||
$commit = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/commits/'.$identifier), true);
|
||||
$composer['time'] = $commit['commit']['committer']['date'];
|
||||
}
|
||||
$this->infoCache[$identifier] = $composer;
|
||||
}
|
||||
|
||||
return $this->infoCache[$identifier];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
if (null === $this->tags) {
|
||||
$tagsData = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/tags'), true);
|
||||
$this->tags = array();
|
||||
foreach ($tagsData as $tag) {
|
||||
$this->tags[$tag['name']] = $tag['commit']['sha'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function getBranches()
|
||||
{
|
||||
if (null === $this->branches) {
|
||||
$branchData = json_decode(file_get_contents('https://api.github.com/repos/'.$this->owner.'/'.$this->repository.'/branches'), true);
|
||||
$this->branches = array();
|
||||
foreach ($branchData as $branch) {
|
||||
$this->branches[$branch['name']] = $branch['commit']['sha'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->branches;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public function hasComposerFile($identifier)
|
||||
{
|
||||
try {
|
||||
$this->getComposerInformation($identifier);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public static function supports($url, $deep = false)
|
||||
{
|
||||
return preg_match('#^(?:https?|git)://github\.com/([^/]+)/(.+?)(?:\.git)?$#', $url, $match);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace Composer\Repository\Vcs;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
interface VcsDriverInterface
|
||||
{
|
||||
/**
|
||||
* Initializes the driver (git clone, svn checkout, fetch info etc)
|
||||
*/
|
||||
function initialize();
|
||||
|
||||
/**
|
||||
* Return the composer.json file information
|
||||
*
|
||||
* @param string $identifier Any identifier to a specific branch/tag/commit
|
||||
* @return array containing all infos from the composer.json file
|
||||
*/
|
||||
function getComposerInformation($identifier);
|
||||
|
||||
/**
|
||||
* Return the root identifier (trunk, master, ..)
|
||||
*
|
||||
* @return string Identifier
|
||||
*/
|
||||
function getRootIdentifier();
|
||||
|
||||
/**
|
||||
* Return list of branches in the repository
|
||||
*
|
||||
* @return array Branch names as keys, identifiers as values
|
||||
*/
|
||||
function getBranches();
|
||||
|
||||
/**
|
||||
* Return list of tags in the repository
|
||||
*
|
||||
* @return array Tag names as keys, identifiers as values
|
||||
*/
|
||||
function getTags();
|
||||
|
||||
/**
|
||||
* @param string $identifier Any identifier to a specific branch/tag/commit
|
||||
* @return array With type, url reference and shasum keys.
|
||||
*/
|
||||
function getDist($identifier);
|
||||
|
||||
/**
|
||||
* @param string $identifier Any identifier to a specific branch/tag/commit
|
||||
* @return array With type, url and reference keys.
|
||||
*/
|
||||
function getSource($identifier);
|
||||
|
||||
/**
|
||||
* Return the URL of the repository
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function getUrl();
|
||||
|
||||
/**
|
||||
* Return true if the repository has a composer file for a given identifier,
|
||||
* false otherwise.
|
||||
*
|
||||
* @param string $identifier Any identifier to a specific branch/tag/commit
|
||||
* @return boolean Whether the repository has a composer file for a given identifier.
|
||||
*/
|
||||
function hasComposerFile($identifier);
|
||||
|
||||
/**
|
||||
* Checks if this driver can handle a given url
|
||||
*
|
||||
* @param string $url
|
||||
* @param Boolean $shallow unless true, only shallow checks (url matching typically) should be done
|
||||
* @return Boolean
|
||||
*/
|
||||
static function supports($url, $deep = false);
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
<?php
|
||||
|
||||
namespace Composer\Repository;
|
||||
|
||||
use Composer\Repository\Vcs\VcsDriverInterface;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
use Composer\Package\Loader\ArrayLoader;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
*/
|
||||
class VcsRepository extends ArrayRepository
|
||||
{
|
||||
protected $url;
|
||||
protected $packageName;
|
||||
|
||||
public function __construct(array $config)
|
||||
{
|
||||
if (!filter_var($config['url'], FILTER_VALIDATE_URL)) {
|
||||
throw new \UnexpectedValueException('Invalid url given for PEAR repository: '.$config['url']);
|
||||
}
|
||||
|
||||
$this->url = $config['url'];
|
||||
}
|
||||
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
$debug = false;
|
||||
|
||||
$drivers = array(
|
||||
'Composer\Repository\Vcs\GitHubDriver',
|
||||
'Composer\Repository\Vcs\GitDriver',
|
||||
'Composer\Repository\Vcs\SvnDriver',
|
||||
);
|
||||
|
||||
foreach ($drivers as $driver) {
|
||||
if ($driver::supports($this->url)) {
|
||||
$driver = new $driver($this->url);
|
||||
$driver->initialize();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$versionParser = new VersionParser;
|
||||
$loader = new ArrayLoader($this->repositoryManager);
|
||||
$versions = array();
|
||||
|
||||
if ($driver->hasComposerFile($driver->getRootIdentifier())) {
|
||||
$data = $driver->getComposerInformation($driver->getRootIdentifier());
|
||||
$this->packageName = !empty($data['name']) ? $data['name'] : null;
|
||||
}
|
||||
|
||||
foreach ($driver->getTags() as $tag => $identifier) {
|
||||
$parsedTag = $this->validateTag($versionParser, $tag);
|
||||
if ($parsedTag && $driver->hasComposerFile($identifier)) {
|
||||
try {
|
||||
$data = $driver->getComposerInformation($identifier);
|
||||
} catch (\Exception $e) {
|
||||
if (strpos($e->getMessage(), 'JSON Parse Error') !== false) {
|
||||
if ($debug) {
|
||||
echo 'Skipped tag '.$tag.', '.$e->getMessage().PHP_EOL;
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
// manually versioned package
|
||||
if (isset($data['version'])) {
|
||||
$data['version_normalized'] = $versionParser->normalize($data['version']);
|
||||
} else {
|
||||
// auto-versionned package, read value from tag
|
||||
$data['version'] = $tag;
|
||||
$data['version_normalized'] = $parsedTag;
|
||||
}
|
||||
|
||||
// make sure tag packages have no -dev flag
|
||||
$data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']);
|
||||
$data['version_normalized'] = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']);
|
||||
|
||||
// broken package, version doesn't match tag
|
||||
if ($data['version_normalized'] !== $parsedTag) {
|
||||
if ($debug) {
|
||||
echo 'Skipped tag '.$tag.', tag ('.$parsedTag.') does not match version ('.$data['version_normalized'].') in composer.json'.PHP_EOL;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
echo 'Importing tag '.$tag.PHP_EOL;
|
||||
}
|
||||
|
||||
$this->addPackage($loader->load($this->preProcess($driver, $data, $identifier)));
|
||||
} elseif ($debug) {
|
||||
echo 'Skipped tag '.$tag.', invalid name or no composer file'.PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($driver->getBranches() as $branch => $identifier) {
|
||||
$parsedBranch = $this->validateBranch($versionParser, $branch);
|
||||
if ($parsedBranch && $driver->hasComposerFile($identifier)) {
|
||||
$data = $driver->getComposerInformation($identifier);
|
||||
|
||||
// manually versioned package
|
||||
if (isset($data['version'])) {
|
||||
$data['version_normalized'] = $versionParser->normalize($data['version']);
|
||||
} else {
|
||||
// auto-versionned package, read value from branch name
|
||||
$data['version'] = $branch;
|
||||
$data['version_normalized'] = $parsedBranch;
|
||||
}
|
||||
|
||||
// make sure branch packages have a -dev flag
|
||||
$normalizedStableVersion = preg_replace('{[.-]?dev$}i', '', $data['version_normalized']);
|
||||
$data['version'] = preg_replace('{[.-]?dev$}i', '', $data['version']) . '-dev';
|
||||
$data['version_normalized'] = $normalizedStableVersion . '-dev';
|
||||
|
||||
// Skip branches that contain a version that has been tagged already
|
||||
foreach ($this->getPackages() as $package) {
|
||||
if ($normalizedStableVersion === $package->getVersion()) {
|
||||
if ($debug) {
|
||||
echo 'Skipped branch '.$branch.', already tagged'.PHP_EOL;
|
||||
}
|
||||
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($debug) {
|
||||
echo 'Importing branch '.$branch.PHP_EOL;
|
||||
}
|
||||
|
||||
$this->addPackage($loader->load($this->preProcess($driver, $data, $identifier)));
|
||||
} elseif ($debug) {
|
||||
echo 'Skipped branch '.$branch.', invalid name or no composer file'.PHP_EOL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function preProcess(VcsDriverInterface $driver, array $data, $identifier)
|
||||
{
|
||||
// keep the name of the main identifier for all packages
|
||||
$data['name'] = $this->packageName ?: $data['name'];
|
||||
|
||||
if (!isset($data['dist'])) {
|
||||
$data['dist'] = $driver->getDist($identifier);
|
||||
}
|
||||
if (!isset($data['source'])) {
|
||||
$data['source'] = $driver->getSource($identifier);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function validateBranch($versionParser, $branch)
|
||||
{
|
||||
try {
|
||||
return $versionParser->normalizeBranch($branch);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function validateTag($versionParser, $version)
|
||||
{
|
||||
try {
|
||||
return $versionParser->normalize($version);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue