drupalorg-1.0.x-dev/src/Drush/Commands/DrushCommands.php
src/Drush/Commands/DrushCommands.php
<?php
namespace Drupal\drupalorg\Drush\Commands;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCache;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Site\Settings;
use Drupal\drupalorg\ProjectService;
use Drupal\drupalorg\Traits\GitLabClientTrait;
use Drupal\drupalorg\Utilities\ActiveInstalls;
use Drupal\drupalorg\Utilities\ComposerNamespace;
use Drupal\drupalorg\Utilities\CoreCompatibility;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
use Drush\Commands\AutowireTrait;
use Drush\Commands\DrushCommands as BaseDrushCommands;
use Drush\Attributes as CLI;
use Drush\Drush;
use Gitlab\ResultPager;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
/**
* A drush command file.
*
* @package Drupal\drupalorg\Commands
*/
final class DrushCommands extends BaseDrushCommands {
use AutowireTrait;
use GitLabClientTrait;
/**
* {@inheritdoc}
*/
public function __construct(
#[Autowire(service: 'entity.memory_cache')]
protected MemoryCache $memoryCache,
#[Autowire(service: 'drupalorg.project_service')]
protected ProjectService $projectService,
protected EntityTypeManagerInterface $entityTypeManager,
protected TimeInterface $time,
) {
parent::__construct();
}
/**
* Calculate the active installs for modules based on the usage table.
*/
#[CLI\Command(name: 'drupalorg:calculate-active-installs')]
public function calculateActiveInstalls() {
ActiveInstalls::calculateNewActiveInstalls(NULL, Drush::verbose());
}
/**
* Calculate the composer namespaces for modules based on update status info.
*
* @see https://www.drupal.org/drupalorg/docs/apis/update-status-xml
*/
#[CLI\Command(name: 'drupalorg:calculate-composer-namespaces')]
public function calculateComposerNamespaces() {
// This is setting a default "drupal/<machine_name>" if not found. Due
// to D7 and below modules. Otherwise, it'll keep trying those every time.
// The below line could be part of an update hook.
ComposerNamespace::calculateNamespace(TRUE);
}
/**
* Calculate the composer compatibility for modules based on their releases.
*/
#[CLI\Command(name: 'drupalorg:calculate-composer-compatibilities')]
public function calculateComposerCompatibilities() {
$last_run = \Drupal::state()->get('drupalorg.composer_compatibilities');
$request_time = \Drupal::time()->getRequestTime();
CoreCompatibility::calculateNewCoreCompatibilities($last_run);
\Drupal::state()->set('drupalorg.composer_compatibilities', $request_time);
}
/**
* Check for disabled GitLab system hooks.
*/
#[CLI\Command(name: 'drupalorg:check-gitlab-system-hooks')]
public function checkGitlabSystemHooks(): int {
$return = self::EXIT_SUCCESS;
foreach ($this->getGitLabClient()->systemHooks()->all() as $hook) {
$this->logger->notice('{url} is {alert_status}, disabled until {disabled_until}', [
'url' => $hook['url'],
'alert_status' => $hook['alert_status'],
'disabled_until' => $hook['disabled_until'],
]);
if ($hook['alert_status'] !== 'executable') {
$this->logger->error('{url} system hook is not executable! API data: {data}', [
'url' => $hook['url'],
'data' => json_encode($hook),
]);
$return = self::EXIT_FAILURE_WITH_CLARITY;
}
}
if ($return === self::EXIT_SUCCESS) {
$this->logger->notice('GitLab system hooks are all enabled!');
}
return $return;
}
/**
* Fetch GitLab avatars and set them as logos where available.
*/
#[CLI\Command(name: 'drupalorg:fetch-gitlab-avatars')]
public function fetchGitlabAvatars(): void {
try {
$client = $this->getGitLabClient();
$project_nids = \Drupal::entityQuery('node')
->accessCheck(FALSE)
->condition('type', ProjectService::PROJECT_TYPES, 'IN')
->notExists('field_logo_url')
->execute();
foreach (array_chunk($project_nids, 100) as $nids) {
foreach (Node::loadMultiple($nids) as $project_node) {
$repository = $this->projectService->getProjectRepositoryInformation($project_node);
if (empty($repository['gitlab_project_id'])) {
continue;
}
$this->logger->notice('Checking project logo for {project}', [
'project' => $project_node->get('field_project_machine_name')->value,
]);
$gitlab_project = $client->projects()->show($repository['gitlab_project_id']);
$this->projectService->updateLogo($project_node, $gitlab_project['avatar_url'] ?? '');
}
$this->memoryCache->deleteAll();
}
}
catch (\Throwable $e) {
$this->logger->error('Could not connect to GitLab or fetch avatars. Code {code} at {at}. Message: {message}', [
'code' => $e->getCode(),
'at' => $e->getFile() . ':' . $e->getLine(),
'message' => $e->getMessage(),
]);
}
}
/**
* Populate milestones for security team.
*/
#[CLI\Command(name: 'drupalorg:security-populate-milestones')]
public function securityPopulateMilestones() {
$gitlab_client = $this->getGitLabClient();
$milestones = array_column((new ResultPager($gitlab_client))->fetchAll($gitlab_client->groupsMilestones(), 'all', ['security']), 'id', 'due_date');
$wednesdays = new \DatePeriod(new \DateTimeImmutable('Wednesday'), new \DateInterval('P7D'), 16);
foreach ($wednesdays as $wednesday) {
$date = $wednesday->format('Y-m-d');
if (!isset($milestones[$date])) {
$label = $date;
$day_of_month = (int) $wednesday->format('j');
if ($day_of_month > 14 && $day_of_month <= 21) {
$label .= ' core';
}
$gitlab_client->groupsMilestones()->create('security', [
'start_date' => $wednesday->sub(new \DateInterval('P6D'))->format('Y-m-d'),
'due_date' => $date,
'title' => $label,
]);
}
}
}
/**
* Calculate organization rank.
*
* While the site is in transition, this only populates
* field_org_rank_components, which the legacy site fetches to complete
* ranking calculations.
*/
#[CLI\Command(name: 'drupalorg:update-organization-rank')]
public function updateOrganizationRank(): void {
// Deltas for field_org_rank_components:
// 0: weighted issue credits, 12 months
// 1: association partnerships
// 2: association memberships
// 3: individual memberships
// 4: case studies
// 5: project(s) supported
// 6: contributor roles
// 7: contribution without issue credits
// 8: contribution with 3 months of issue credit
// 9: contribution with 12 months of issue credit
// 10: Event contribution
// 11: Weighted security advisories, 12 months.
$weights = Settings::get('drupalorg_organization_rank_weights');
// Batch load case studies. Index by organization nid and Drupal version.
$case_studies = [];
$case_study_nids = $this->entityTypeManager->getStorage('node')->getQuery()
->accessCheck(TRUE)
->condition('type', 'casestudy')
->condition('status', NodeInterface::PUBLISHED)
->execute();
foreach (array_chunk($case_study_nids, 100) as $nids) {
foreach (Node::loadMultiple($nids) as $node) {
foreach ($node->get('field_case_organizations')->getValue() as $organization) {
foreach ($node->get('field_drupal_version')->referencedEntities() as $version) {
$case_studies[$organization['target_id']][$version->label()] = ($case_studies[$organization['target_id']][$version->label()] ?? 0) + 1;
}
}
}
}
// Iterate over organizations.
$organization_nids = $this->entityTypeManager->getStorage('node')->getQuery()
->accessCheck(FALSE)
->condition('type', 'organization')
->execute();
foreach (Node::loadMultiple($organization_nids) as $organization_node) {
$this->logger->info('Updating rank for {title} ({nid})', [
'title' => $organization_node->getTitle(),
'nid' => $organization_node->id(),
]);
$new_rank = array_fill(0, 12, 0);
$updated = FALSE;
// Case studies.
foreach ($weights['case study'] as $drupal_version => $weight) {
$new_rank[4] += ($case_studies[$organization_node->id()][$drupal_version] ?? 0) * $weight;
}
// Update if needed.
foreach ($new_rank as $delta => $value) {
if ($organization_node->get('field_org_rank_components')->get($delta)?->getValue()['value'] != $value) {
$organization_node->set('field_org_rank_components', $new_rank);
$updated = TRUE;
break;
}
}
if ($updated) {
$organization_node->setSyncing(TRUE);
$organization_node->save();
}
}
}
}
