drupalorg-1.0.x-dev/src/Controller/IssueForksController.php
src/Controller/IssueForksController.php
<?php
namespace Drupal\drupalorg\Controller;
use Drupal\Component\Utility\Html;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Link;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\Render\Renderer;
use Drupal\drupalorg\Traits\GitLabClientTrait;
use Drupal\drupalorg\UserService;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Controller to manage issue forks, merge requests, etc.
*/
class IssueForksController extends ControllerBase {
use GitLabClientTrait;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('queue'),
$container->get('drupalorg.user_service'),
$container->get('renderer')
);
}
/**
* Construct method.
*
* @param \Drupal\Core\Queue\QueueFactory $queueFactory
* Queue factory.
* @param \Drupal\drupalorg\UserService $userService
* User service from drupalorg module.
* @param \Drupal\Core\Render\Renderer $renderer
* Renderer service.
*/
public function __construct(
protected QueueFactory $queueFactory,
protected UserService $userService,
protected Renderer $renderer,
) {
}
/**
* Check branches of an issue fork.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* Branches information.
*/
public function issueForkCheckBranches(Request $request): JsonResponse {
$fork_id_param = $request->query->get('fork_id');
$source_link_param = $request->query->get('source_link');
$existing_branches_param = $request->query->all('existing_branches');
$data = [
'status' => FALSE,
'fork_id' => $fork_id_param,
'branches' => [],
'new_branches' => FALSE,
'message' => '',
];
if (empty($fork_id_param) || empty($source_link_param)) {
$data['error'] = '"fork_id" and "source_link" are required';
return new JsonResponse($data);
}
[
'project' => $project_id,
'issue_iid' => $issue_iid,
'error' => $error,
] = $this->getProjectAndIssueIdFromUrl($source_link_param);
try {
// Get issue specific branches.
$issue_specific_branches = $this->getGitLabClient()->repositories()->branches($fork_id_param, [
'search' => '^' . $issue_iid . '-',
]);
$data['branches'] = $issue_specific_branches;
if (!empty($issue_specific_branches)) {
$issue_specific_branches_names = array_map(fn($i) => $i['name'], $issue_specific_branches);
// rsort($existing_branches_param);
// Compare existing with newly returned to see if there are new ones.
$data['new_branches'] = !empty(array_diff($issue_specific_branches_names, $existing_branches_param));
}
$data['status'] = TRUE;
}
catch (\Throwable $e) {
$data['message'] = 'Could not check branches.';
$this->getLogger('drupalorg')->error('Error checking branches in fork. Message: @message', [
'@message' => $e->getMessage(),
]);
}
return new JsonResponse($data);
}
/**
* Check access to an issue fork.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* Whether the user has access to the fork or not in JSON format.
*/
public function issueForkCheckAccess(Request $request): JsonResponse {
$fork_id_param = $request->query->get('fork_id');
$data = [
'access' => FALSE,
'fork_id' => $fork_id_param,
'message' => '',
];
if (empty($fork_id_param)) {
$data['error'] = '"fork_id" is required';
return new JsonResponse($data);
}
$gitlab_user_id = $this->userService->getGitLabUserId($this->currentUser());
if (empty($gitlab_user_id)) {
$data['error'] = 'You need to set up your git username in your profile';
return new JsonResponse($data);
}
try {
$member = $this->getGitLabClient()->projects()->member($fork_id_param, $gitlab_user_id);
$data['access'] = !empty($member);
}
catch (\Throwable $e) {
$data['message'] = 'Not allowed or fork not found';
$this->getLogger('drupalorg')->error('Error checking member in fork. Message: @message', [
'@message' => $e->getMessage(),
]);
}
return new JsonResponse($data);
}
/**
* Grant access to an issue fork.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse
* Whether the user has access to the fork or not in JSON format.
*/
public function issueForkRequestAccess(Request $request): JsonResponse|RedirectResponse {
$format_param = $request->query->get('format');
$fork_id_param = $request->query->get('fork_id');
$source_link_param = $request->query->get('source_link');
$data = [
'access' => FALSE,
'fork_id' => $fork_id_param,
'message' => '',
];
if (empty($fork_id_param)) {
if ($format_param === 'json') {
$data['error'] = '"fork_id" is required';
return new JsonResponse($data);
}
$this->messenger()->addError($this->t('"fork_id" is required'));
return $this->redirect('drupalorg.issue_fork_management', [], [
'query' => [
'source_link' => $source_link_param,
],
]);
}
$gitlab_user_id = $this->userService->getGitLabUserId($this->currentUser());
if (empty($gitlab_user_id)) {
$data['error'] = 'You need to set up your git username in your profile';
return new JsonResponse($data);
}
if ($this->addUserToFork($fork_id_param, $gitlab_user_id)) {
$data['access'] = TRUE;
}
else {
$data['message'] = $this->t('Could not grant access. Try again later or review your git username.');
}
if ($format_param === 'json') {
return new JsonResponse($data);
}
// Non-json. Show messages and do redirects instead.
if ($data['access']) {
$this->messenger()->addStatus($this->t('Access granted'));
}
else {
$this->messenger()->addError($data['message']);
}
return $this->redirect('drupalorg.issue_fork_management', [], [
'query' => [
'source_link' => $source_link_param,
],
]);
}
/**
* Creates a new fork.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array|\Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\JsonResponse
* Render array or redirect.
*/
public function issueForkCreateFork(Request $request): JsonResponse | RedirectResponse | array {
$format_param = $request->query->get('format');
$with_branch_param = $request->query->get('with_branch');
$branch_name_param = $request->query->get('branch_name');
$source_branch_param = $request->query->get('source_branch');
$source_link_param = $request->query->get('source_link');
[
'project' => $project_id,
'issue_iid' => $issue_iid,
'error' => $error,
] = $this->getProjectAndIssueIdFromUrl($source_link_param);
$data = [
'created' => FALSE,
'fork' => NULL,
'message' => '',
];
if (!empty($error)) {
if ($format_param === 'json') {
$data['message'] = $this->t('Something went wrong');
return new JsonResponse($data);
}
$this->messenger()->addError($error);
return [
'#title' => $this->t('Forks management'),
'#markup' => $this->t('Something went wrong.'),
];
}
// Make source that the branch name is correct.
$branch_name_param = !empty($branch_name_param) ?
substr(trim(Html::getUniqueId($branch_name_param), '-'), 0, 50) :
'issue-branch';
$branch_name = $with_branch_param ? $issue_iid . '-' . $branch_name_param : NULL;
$gitlab_user_id = $this->userService->getGitLabUserId($this->currentUser());
$fork_created = NULL;
try {
$fork = $this->createFork($source_link_param, $gitlab_user_id);
if (!empty($fork['name'])) {
// Update the issue with a link for fork management.
$this->getGitLabClient()->issues()->addNote(
$project_id,
$issue_iid,
$this->automatedNoteHeading($this->t('Fork created:')) . $this->t("A [fork](@fork) was created for this issue. Go to the @link page for this issue to find the git commands to checkout a branch on this fork. React to this message with :heavy_plus_sign: to gain access.", [
'@fork' => $fork['web_url'],
'@link' => Link::createFromRoute(t('fork management'), 'drupalorg.issue_fork_management', [],
[
'query' => ['source_link' => $source_link_param],
'absolute' => TRUE,
]
)->toString(),
])
);
// This part is not relevant to creating the fork, so queue it.
$this->queueFactory->get('drupalorg_issue_forks_queue_worker')->createItem([
'action' => 'post_fork_creation',
'fork_id' => $fork['id'],
'user_id' => $this->currentUser()->id(),
'issue_id' => $issue_iid,
'issue_link' => $source_link_param,
'source_branch' => $source_branch_param,
'branch_name' => $branch_name,
]);
if ($format_param === 'json') {
$data['created'] = TRUE;
$data['fork'] = $fork;
return new JsonResponse($data);
}
// Let the users know what happened.
$fork_created = $fork['name'];
$this->messenger()->addStatus($this->t('The fork "@fork" was created successfully.', [
'@fork' => $fork_created,
]));
if (empty($fork['user_added'])) {
$this->messenger()->addWarning($this->t('Could not add your user as member of the fork, try using the "Request access" button.'));
}
}
else {
$this->messenger()->addWarning($this->t('Creating the fork failed.'));
}
}
catch (\Throwable $e) {
$this->messenger()->addError($this->t('Fork could not be created.'));
$this->getLogger('drupalorg')->error('Error creating fork. Message: @message', [
'@message' => $e->getMessage(),
]);
$data['message'] = $this->t('Fork could not be created');
}
if ($format_param === 'json') {
$this->messenger()->deleteAll();
return new JsonResponse($data);
}
return $this->redirect('drupalorg.issue_fork_management', [], [
'query' => [
'source_link' => $source_link_param,
'fork_created' => $fork_created ?? '',
],
]);
}
/**
* Page to manage all issue forks, MRs, access, etc.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return array|\Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\Response
* Render array or response.
*/
public function issueForksManagement(Request $request): array | JsonResponse | Response {
$format_param = $request->query->get('format');
$source_link_param = $request->query->get('source_link');
[
'project' => $project_id,
'issue_iid' => $issue_iid,
'error' => $error,
] = $this->getProjectAndIssueIdFromUrl($source_link_param);
if (!empty($error)) {
if ($format_param === 'json') {
return new JsonResponse([
'status' => FALSE,
'message' => 'Something went wrong.',
]);
}
$this->messenger()->addError($error);
return [
'#title' => $this->t('Forks management'),
'#markup' => $this->t('Something went wrong.'),
];
}
$client = $this->getGitLabClient();
$issue = NULL;
$forks = NULL;
$branches = NULL;
$issue_specific_branches = NULL;
$merge_requests = NULL;
$project = NULL;
$status = FALSE;
try {
// Load the empty template and then load these via a separate request.
if ($format_param === 'json' || $format_param === 'html' || $format_param === 'all') {
$project = $client->projects()->show($project_id);
$issue = $client->issues()->show($project_id, $issue_iid);
$branches = $client->repositories()->branches($project_id);
$merge_requests = $client->issues()->relatedMergeRequests($project_id, $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) {
unset($forks[$index]);
}
else {
// Get issue specific branches.
$issue_specific_branches[$fork['id']] = $client->repositories()->branches($fork['id'], [
'search' => '^' . $issue_iid . '-',
]);
}
}
}
$status = TRUE;
}
}
catch (\Throwable $e) {
// Let the front-end display that there is no data.
$this->getLogger('drupalorg')->error('Error getting GitLab info. Message: @message', [
'@message' => $e->getMessage(),
]);
}
if (empty($issue) && $format_param === 'all') {
$this->messenger()->addWarning($this->t('There was a problem finding the issue. Please make sure that the link provided is valid.'));
}
// At this point we have all the data. See the requested format and return.
if ($format_param === 'json') {
return new JsonResponse([
'status' => $status,
'forks' => $forks,
'branches' => $branches,
'issue_specific_branches' => $issue_specific_branches,
'merge_requests' => $merge_requests,
'issue' => $issue,
'source_link' => $source_link_param,
'project' => $project,
'logged_in' => $this->currentUser()->isAuthenticated(),
]);
}
// Full page return.
$build = [
'#title' => $this->t('Forks management') . ($issue ? ': ' . $issue['title'] : ''),
'#theme' => 'drupalorg_issue_forks_management',
'#partial' => ($format_param === 'html'),
'#forks' => $forks,
'#branches' => $branches,
'#issue_specific_branches' => $issue_specific_branches,
'#merge_requests' => $merge_requests,
'#issue' => $issue,
'#source_link' => $source_link_param,
'#project' => $project,
'#logged_in' => $this->currentUser()->isAuthenticated(),
'#attached' => [
'library' => [
'drupalorg/forks_management',
'bluecheese/forks_management',
// SDCs used in the markup and loaded via AJAX.
'core/components.bluecheese--copy_code_block',
],
],
'#cache' => [
'max-age' => 0,
'contexts' => [
'url',
'user',
],
],
];
// Partial page return.
if ($format_param === 'html') {
// Title, libraries and caching information is for full page mode.
unset($build['#title']);
unset($build['#attached']);
unset($build['#cache']);
// Render without the layout elements, just the block.
$output = $this->renderer->renderRoot($build);
$response = new Response();
$response->setContent($output);
return $response;
}
return $build;
}
}
