Skip to content
Snippets Groups Projects
Commit 71497518 authored by Ted Bowman's avatar Ted Bowman
Browse files

Issue #3307163 by tedbow, TravisCarden: Add the ability to ProjectInfo handle...

Issue #3307163 by tedbow, TravisCarden: Add the ability to ProjectInfo handle projects not in the active codebase
parent 963df7bb
No related branches found
No related tags found
No related merge requests found
......@@ -168,3 +168,6 @@ services:
class: Drupal\package_manager\Validator\ComposerPatchesValidator
tags:
- { name: event_subscriber }
package_manager.update_processor:
class: Drupal\package_manager\PackageManagerUpdateProcessor
arguments: [ '@config.factory', '@queue', '@update.fetcher', '@state', '@private_key', '@keyvalue', '@keyvalue.expirable' ]
<?php
namespace Drupal\package_manager;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\PrivateKey;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\State\StateInterface;
use Drupal\update\UpdateFetcherInterface;
use Drupal\update\UpdateProcessor;
/**
* Extends the Update module's update processor allow fetching any project.
*
* The Update module's update processor service is intended to only fetch
* information for projects in the active codebase. Although it would be
* possible to use the Update module's update processor service to fetch
* information for projects not in the active code base this would add the
* project information to Update module's cache which would result in these
* projects being returned from the Update module's global functions such as
* update_get_available().
*/
class PackageManagerUpdateProcessor extends UpdateProcessor {
/**
* Constructs an PackageManagerUpdateProcessor object.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Queue\QueueFactory $queue_factory
* The queue factory.
* @param \Drupal\update\UpdateFetcherInterface $update_fetcher
* The update fetcher service.
* @param \Drupal\Core\State\StateInterface $state_store
* The state service.
* @param \Drupal\Core\PrivateKey $private_key
* The private key factory service.
* @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
* The key/value factory.
* @param \Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface $key_value_expirable_factory
* The expirable key/value factory.
*/
public function __construct(ConfigFactoryInterface $config_factory, QueueFactory $queue_factory, UpdateFetcherInterface $update_fetcher, StateInterface $state_store, PrivateKey $private_key, KeyValueFactoryInterface $key_value_factory, KeyValueExpirableFactoryInterface $key_value_expirable_factory) {
$this->updateFetcher = $update_fetcher;
$this->updateSettings = $config_factory->get('update.settings');
$this->fetchQueue = $queue_factory->get('package_manager.update_fetch_tasks');
$this->tempStore = $key_value_expirable_factory->get('package_manager.update');
$this->fetchTaskStore = $key_value_factory->get('package_manager.update_fetch_task');
$this->availableReleasesTempStore = $key_value_expirable_factory->get('package_manager.update_available_releases');
$this->stateStore = $state_store;
$this->privateKey = $private_key;
$this->fetchTasks = [];
$this->failed = [];
}
/**
* Gets the project data by name.
*
* @param string $name
* The project name.
*
* @return mixed[]
* The project data if any is available, otherwise NULL.
*/
public function getProjectData(string $name): ?array {
if ($this->availableReleasesTempStore->has($name)) {
return $this->availableReleasesTempStore->get($name);
}
$project_fetch_data = [
'name' => $name,
'project_type' => 'unknown',
'includes' => [],
];
$this->createFetchTask($project_fetch_data);
if ($this->processFetchTask($project_fetch_data)) {
// If the fetch task was successful return the project information.
return $this->availableReleasesTempStore->get($name);
}
return NULL;
}
/**
* {@inheritdoc}
*/
public function processFetchTask($project) {
// The parent method will set 'update.last_check' which will be used to
// inform the user when the last time update information was checked. In
// order to leave this value unaffected we will reset this to it's previous
// value.
$last_check = $this->stateStore->get('update.last_check');
$success = parent::processFetchTask($project);
$this->stateStore->set('update.last_check', $last_check);
return $success;
}
}
......@@ -74,9 +74,12 @@ final class ProjectInfo {
* If data about available updates cannot be retrieved.
*/
public function getProjectInfo(): ?array {
$available_updates = update_get_available(TRUE);
$available_updates = $this->getAvailableProjects();
$project_data = update_calculate_project_data($available_updates);
return $project_data[$this->name] ?? NULL;
if (!isset($project_data[$this->name])) {
return $available_updates[$this->name] ?? NULL;
}
return $project_data[$this->name];
}
/**
......@@ -99,31 +102,39 @@ final class ProjectInfo {
if (!$project) {
return NULL;
}
$available_updates = update_get_available()[$this->name];
$available_updates = $this->getAvailableProjects()[$this->name];
if ($available_updates['project_status'] !== 'published') {
throw new \RuntimeException("The project '{$this->name}' can not be updated because its status is " . $available_updates['project_status']);
}
$installed_version = $this->getInstalledVersion();
// If we're already up-to-date, there's nothing else we need to do.
if ($project['status'] === UpdateManagerInterface::CURRENT) {
return [];
}
if ($installed_version) {
// If the project is installed, and we're already up-to-date, there's
// nothing else we need to do.
if ($project['status'] === UpdateManagerInterface::CURRENT) {
return [];
}
if (empty($available_updates['releases'])) {
// If project is not current we should always have at least one release.
throw new \RuntimeException('There was a problem getting update information. Try again later.');
if (empty($available_updates['releases'])) {
// If project is installed but not current we should always have at
// least one release.
throw new \RuntimeException('There was a problem getting update information. Try again later.');
}
}
$installed_version = $this->getInstalledVersion();
$support_branches = explode(',', $available_updates['supported_branches']);
$installable_releases = [];
foreach ($available_updates['releases'] as $release_info) {
$release = ProjectRelease::createFromArray($release_info);
$version = $release->getVersion();
$semantic_version = LegacyVersionUtility::convertToSemanticVersion($version);
$semantic_installed_version = LegacyVersionUtility::convertToSemanticVersion($installed_version);
if (Comparator::lessThanOrEqualTo($semantic_version, $semantic_installed_version)) {
// Stop searching for releases as soon as we find the installed version.
break;
if ($installed_version) {
$semantic_version = LegacyVersionUtility::convertToSemanticVersion($version);
$semantic_installed_version = LegacyVersionUtility::convertToSemanticVersion($installed_version);
if (Comparator::lessThanOrEqualTo($semantic_version, $semantic_installed_version)) {
// If the project is installed stop searching for releases as soon as
// we find the installed version.
break;
}
}
if ($this->isInstallable($release, $support_branches)) {
$installable_releases[$version] = $release;
......@@ -141,12 +152,37 @@ final class ProjectInfo {
*/
public function getInstalledVersion(): ?string {
if ($project_data = $this->getProjectInfo()) {
if (empty($project_data['existing_version'])) {
throw new \UnexpectedValueException("Project '{$this->name}' does not have 'existing_version' set");
}
return $project_data['existing_version'];
return $project_data['existing_version'] ?? NULL;
}
return NULL;
}
/**
* Gets the available projects.
*
* @return array
* The available projects keyed by project machine name in the format
* returned by update_get_available(). If the project specified in ::name is
* not returned from update_get_available() this project will be explicitly
* fetched and added the return value of this function.
*
* @see \update_get_available()
*/
private function getAvailableProjects(): array {
$available_projects = update_get_available(TRUE);
// update_get_available() will only returns projects that are in the active
// codebase. If the project specified by ::name is not returned in
// $available_projects, it means it is not in the active codebase, therefore
// we will retrieve the project information from Package Manager's own
// update processor service.
if (!isset($available_projects[$this->name])) {
/** @var \Drupal\package_manager\PackageManagerUpdateProcessor $update_processor */
$update_processor = \Drupal::service('package_manager.update_processor');
if ($project_data = $update_processor->getProjectData($this->name)) {
$available_projects[$this->name] = $project_data;
}
}
return $available_projects;
}
}
......@@ -19,7 +19,7 @@ class ComposerUtilityTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['package_manager'];
protected static $modules = ['package_manager', 'update'];
/**
* {@inheritdoc}
......
......@@ -17,7 +17,7 @@ class FileSyncerFactoryTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['package_manager'];
protected static $modules = ['package_manager', 'update'];
/**
* Data provider for testFactory().
......
<?php
namespace Drupal\Tests\package_manger\Kernel;
namespace Drupal\Tests\package_manager\Kernel;
use Drupal\package_manager\ProjectInfo;
use Drupal\Tests\package_manager\Kernel\PackageManagerKernelTestBase;
/**
* @coversDefaultClass \Drupal\package_manager\ProjectInfo
......@@ -118,6 +117,52 @@ class ProjectInfoTest extends PackageManagerKernelTestBase {
];
}
/**
* Tests a project that is not in the codebase.
*/
public function testNewProject(): void {
$fixtures_directory = __DIR__ . '/../../fixtures/release-history/';
$metadata_fixtures['drupal'] = $fixtures_directory . 'drupal.9.8.2.xml';
$metadata_fixtures['aaa_automatic_updates_test'] = $fixtures_directory . 'aaa_automatic_updates_test.9.8.2.xml';
$this->setReleaseMetadata($metadata_fixtures);
$available = update_get_available(TRUE);
$this->assertSame(['drupal'], array_keys($available));
$this->setReleaseMetadata($metadata_fixtures);
$state = $this->container->get('state');
// Set the state that the update module uses to store last checked time
// ensure our calls do not affect it.
$state->set('update.last_check', 123);
$project_info = new ProjectInfo('aaa_automatic_updates_test');
$project_data = $project_info->getProjectInfo();
// Ensure the project information is correct.
$this->assertSame('AAA', $project_data['title']);
$all_releases = [
'7.0.1',
'7.0.0',
'7.0.0-alpha1',
'8.x-6.2',
'8.x-6.1',
'8.x-6.0',
'8.x-6.0-alpha1',
'7.0.x-dev',
'8.x-6.x-dev',
];
$uninstallable_releases = ['7.0.x-dev', '8.x-6.x-dev'];
$installable_releases = array_values(array_diff($all_releases, $uninstallable_releases));
$this->assertSame(
$all_releases,
array_keys($project_data['releases'])
);
$this->assertSame(
$installable_releases,
array_keys($project_info->getInstallableReleases())
);
$this->assertNull($project_info->getInstalledVersion());
// Ensure we have not changed the state the update module uses to store
// the last checked time.
$this->assertSame(123, $state->get('update.last_check'));
}
/**
* Tests a project with a status other than "published".
*
......
......@@ -18,7 +18,7 @@ class ServicesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['package_manager'];
protected static $modules = ['package_manager', 'update'];
/**
* Tests that Package Manager's public services can be instantiated.
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment