drupalorg-1.0.x-dev/src/Traits/GitLabClientTrait.php

src/Traits/GitLabClientTrait.php
<?php

namespace Drupal\drupalorg\Traits;

use Drupal\Component\Utility\Html;
use Drupal\contribution_records\SourceLink\GitDrupalCodeBase;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Gitlab\Client;

/**
 * Helper function to get a Gitlab client instance.
 *
 * @todo Use https://git.drupalcode.org/project/gitlab_api instead.
 */
trait GitLabClientTrait {

  use StringTranslationTrait;

  /**
   * Return a gitlab client instance.
   *
   * @return \Gitlab\Client
   *   Client instance.
   */
  protected function getGitLabClient(): Client {
    $config = \Drupal::config('drupalorg.gitlab_settings');
    if (
      empty($config->get('host')) ||
      empty($config->get('token')) ||
      $config->get('token') === 'CHANGE-ME'
    ) {
      throw new \Exception('GitLab host or token not set.');
    }
    $gitlab_client = new Client();
    $gitlab_client->setUrl($config->get('host'));
    $gitlab_client->authenticate($config->get('token'), Client::AUTH_HTTP_TOKEN);

    return $gitlab_client;
  }

  /**
   * Returns the URL of the instance of GitLab in use.
   *
   * @return string
   *   URL of the instance.
   */
  public function getGitLabUrl(): string {
    return \Drupal::config('drupalorg.gitlab_settings')->get('host');
  }

  /**
   * Returns the heading to prepend to issue notes sent by the system.
   *
   * @param string $heading
   *   Heading string.
   *
   * @return string
   *   Heading string formatted as a note.
   */
  public function automatedNoteHeading(string $heading = 'Drupal.org'): string {
    $heading = Html::escape($heading);
    $comment = $this->hiddenComment();
    return ">>> [!note] $heading\n<br>$comment\n";
  }

  /**
   * Returns a hidden comment to insert and detect in some processes.
   *
   * @return string
   *   Hidden comment.
   */
  public function hiddenComment(): string {
    return '<!-- Drupal.org comment -->';
  }

  /**
   * Find the fork for the given issue.
   *
   * @param string $issue_link
   *   Issue link.
   *
   * @return array
   *   Fork information.
   */
  protected function findFork(string $issue_link): array {
    $return = [];

    try {
      [
        'project' => $project_id,
        'issue_iid' => $issue_iid,
        'error' => $error,
      ] = $this->getProjectAndIssueIdFromUrl($issue_link);
      $client = $this->getGitLabClient();
      $project = $client->projects()->show($project_id);
      if ($project && $issue_iid) {
        $fork_name_pattern = $project['path'] . '-' . $issue_iid;
        $forks = $client->projects()->forks($project_id, [
          'search' => $fork_name_pattern,
        ]);
        if (!empty($forks)) {
          foreach ($forks as $index => $fork) {
            // Returned forks need to be 100% match. eg: abc-1 vs abc-11.
            if ($fork['name'] === $fork_name_pattern) {
              $return = $fork;
            }
          }
        }
      }
    }
    catch (\Throwable $e) {
      // Could not find it. Function will fail in the next check.
    }

    return $return;
  }

  /**
   * Adds the current user to a fork as member.
   *
   * @param int $fork_id
   *   ID of the fork.
   * @param string $gitlab_user_id
   *   GitLab user ID.
   *
   * @return bool
   *   Whether the user was added or not.
   */
  protected function addUserToFork($fork_id, $gitlab_user_id): bool {
    if (!is_numeric($fork_id)) {
      return FALSE;
    }

    $added = FALSE;
    try {
      $client = $this->getGitLabClient();
      $client->projects()->addMember(
        $fork_id,
        $gitlab_user_id,
        $this->getForkAccessLevel()
      );
      $added = TRUE;
    }
    catch (\Throwable $e) {
      if ($e->getMessage() !== 'Member already exists') {
        \Drupal::logger('drupalorg')->error('Error adding member to fork. Message: @message', [
          '@message' => $e->getMessage(),
        ]);
      }
      else {
        $added = TRUE;
      }
    }

    return $added;
  }

  /**
   * Creates a fork.
   *
   * @param string $source_link_param
   *   Source link.
   * @param int $gitlab_user_id
   *   GitLab's user ID.
   *
   * @return array
   *   Fork created.
   */
  protected function createFork($source_link_param, $gitlab_user_id): array {
    $fork = [];
    try {
      [
        'project' => $project_id,
        'issue_iid' => $issue_iid,
        'error' => $error,
      ] = $this->getProjectAndIssueIdFromUrl($source_link_param);
      $client = $this->getGitLabClient();
      $project = $client->projects()->show($project_id);
      $fork_name = $project['path'] . '-' . $issue_iid;
      $fork = $client->projects()->fork($project_id, [
        'namespace' => 'issue',
        'path' => $fork_name,
        'name' => $fork_name,
      ]);
      if (!empty($fork['name'])) {
        // Try adding the user to the fork as well.
        $fork['user_added'] = $this->addUserToFork($fork['id'], $gitlab_user_id);
      }
    }
    catch (\Throwable $e) {
      \Drupal::logger('drupalorg')->error('Error creating fork. Message: @message', [
        '@message' => $e->getMessage(),
      ]);
    }

    return $fork;
  }

  /**
   * Default access level for fork members.
   *
   * @see https://docs.gitlab.com/api/access_requests/#valid-access-levels
   *
   * @return string
   *   Access level within the fork.
   */
  protected function getForkAccessLevel(): string {
    return '30';
  }

  /**
   * Default options for forks.
   *
   * @return array
   *   Options for fork creation.
   */
  protected function getForkDefaultOptions(): array {
    return [
      'merge_requests_access_level' => 'enabled',
      'squash_option' => 'default_on',
      'issues_access_level' => 'disabled',
      'forking_access_level' => 'disabled',
      'builds_access_level' => 'enabled',
      'wiki_access_level' => 'disabled',
      'snippets_access_level' => 'disabled',
      'pages_access_level' => 'disabled',
      'operations_access_level' => 'disabled',
      'monitor_access_level' => 'disabled',
      'environments_access_level' => 'disabled',
      'feature_flags_access_level' => 'disabled',
      'infrastructure_access_level' => 'disabled',
      'releases_access_level' => 'disabled',
      'container_registry_enabled' => FALSE,
      'shared_runners_enabled' => TRUE,
      'request_access_enabled' => FALSE,
      'auto_devops_enabled' => FALSE,
      'packages_enabled' => FALSE,
      'service_desk_enabled' => FALSE,
      'remove_source_branch_after_merge' => FALSE,
    ];
  }

  /**
   * Parses the URL and extracts the project and issue id from it.
   *
   * @param string|null $source_link_param
   *   Link to parse.
   *
   * @return array
   *   Array containing the project and the issue id.
   */
  protected function getProjectAndIssueIdFromUrl(?string $source_link_param = NULL): array {
    $info = [
      'project' => NULL,
      'issue_iid' => NULL,
      'error' => NULL,
    ];

    if (empty($source_link_param)) {
      $info['error'] = $this->t('"source_link" parameter is required.');
      return $info;
    }

    // Format: https://git.drupalcode.org/project/config_notify/-/issues/10
    $url_parts = parse_url($source_link_param);

    // Only Gitlab issues have the fork management done via this page. If the
    // source link is an MR, that means that the forking and branching is taken
    // care of, and for issues coming from www.drupal.org, the fork management
    // is still done via the issue page.
    $contribution_records_config = \Drupal::config('contribution_records.settings');
    $accepted_domains = [GitDrupalCodeBase::DOMAIN];
    if ($contribution_records_config->get('allow_dev_sources')) {
      $accepted_domains[] = $contribution_records_config->get('gitlab_dev_source');
    }
    if (!in_array($url_parts['host'], $accepted_domains)) {
      $info['error'] = $this->t('"source_link" has an invalid domain. Only issues from the following domains are accepted: @domains.', [
        '@domains' => implode(', ', $accepted_domains),
      ]);
      return $info;
    }

    if (
      empty($url_parts['host']) ||
      empty($url_parts['scheme']) ||
      empty($url_parts['path']) ||
      (
        // Valid namespace check.
        !str_starts_with($url_parts['path'], '/project/') &&
        !str_starts_with($url_parts['path'], '/sandbox/')
      ) ||
      !str_contains($url_parts['path'], '-/issues/')
    ) {
      $info['error'] = $this->t('"source_link" is not a valid link.');
      return $info;
    }

    // URL seems valid, extract the project name and the issue id.
    $path = explode('/', trim($url_parts['path'], '/'));
    if (
      count($path) !== 5 ||
      (
        $path[0] !== 'project' &&
        $path[0] !== 'sandbox'
      ) ||
      $path[2] !== '-' ||
      $path[3] !== 'issues' ||
      !is_numeric($path[4])
    ) {
      $info['error'] = $this->t('"source_link" is not a valid link.');
      return $info;
    }

    // We know for sure that the URL is well formatted.
    $info['project'] = $path[0] . '/' . $path[1];
    $info['issue_iid'] = (int) $path[4];

    return $info;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc