Commit cf3477d2 authored by Phil Hughes's avatar Phil Hughes Committed by Mayra Cabrera

Move core merge request widget state into GraphQL

This moves the main core merge request widget state into
GraphQL.
This is part of a bigger effort to eventually have the
whole merge request widget in GraphQL.

Note: This is behind a feature flag for now.

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/235717
parent f174031b
......@@ -55,13 +55,15 @@ export default {
},
methods: {
removeWipMutation() {
const { mergeRequestQueryVariables } = this;
this.isMakingRequest = true;
this.$apollo
.mutate({
mutation: removeWipMutation,
variables: {
...this.mergeRequestQueryVariables,
...mergeRequestQueryVariables,
wip: false,
},
update(
......@@ -83,14 +85,14 @@ export default {
const data = store.readQuery({
query: getStateQuery,
variables: this.mergeRequestQueryVariables,
variables: mergeRequestQueryVariables,
});
data.project.mergeRequest.workInProgress = workInProgress;
data.project.mergeRequest.title = title;
store.writeQuery({
query: getStateQuery,
data,
variables: this.mergeRequestQueryVariables,
variables: mergeRequestQueryVariables,
});
},
optimisticResponse: {
......
......@@ -96,12 +96,11 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
result({
data: {
project: { mergeRequest },
},
}) {
this.mr.setGraphqlData(mergeRequest);
result({ data: { project } }) {
if (project) {
this.mr.setGraphqlData(project);
this.loading = false;
}
},
},
},
......@@ -120,9 +119,17 @@ export default {
mr: store,
state: store && store.state,
service: store && this.createService(store),
loading: true,
};
},
computed: {
isLoaded() {
if (window.gon?.features?.mergeRequestWidgetGraphql) {
return !this.loading;
}
return this.mr;
},
shouldRenderApprovals() {
return this.mr.state !== 'nothingToMerge';
},
......@@ -409,7 +416,7 @@ export default {
};
</script>
<template>
<div v-if="mr" class="mr-state-widget gl-mt-3">
<div v-if="isLoaded" class="mr-state-widget gl-mt-3">
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
......
query getState($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
archived
onlyAllowMergeIfPipelineSucceeds
mergeRequest(iid: $iid) {
title
autoMergeEnabled
commitCount
conflicts
diffHeadSha
mergeError
mergeStatus
mergeableDiscussionsState
pipelines(first: 1) {
nodes {
status
}
}
shouldBeRebased
sourceBranchExists
targetBranchExists
userPermissions {
canMerge
}
workInProgress
}
}
......
......@@ -43,12 +43,10 @@ export default class MergeRequestStore {
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path;
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.default_merge_commit_message;
this.shortMergeCommitSha = data.short_merged_commit_sha;
this.mergeCommitSha = data.merged_commit_sha;
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.pipelineCoverageDelta = data.pipeline_coverage_delta;
......@@ -61,9 +59,6 @@ export default class MergeRequestStore {
this.rebaseInProgress = data.rebase_in_progress;
this.mergeRequestDiffsPath = data.diffs_path;
this.approvalsWidgetType = data.approvals_widget_type;
this.projectArchived = data.project_archived;
this.branchMissing = data.branch_missing;
this.hasConflicts = data.has_conflicts;
if (data.issues_links) {
const links = data.issues_links;
......@@ -81,25 +76,18 @@ export default class MergeRequestStore {
this.setToAutoMergeBy = MergeRequestStore.formatUserObject(data.merge_user || {});
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.mergeError = data.merge_error;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = data.remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
this.autoMergeStrategy = data.auto_merge_strategy;
this.availableAutoMergeStrategies = data.available_auto_merge_strategies;
this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy(
this.availableAutoMergeStrategies,
);
this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = Boolean(data.should_be_rebased);
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.mergeRequestState = data.state;
this.isOpen = this.mergeRequestState === 'opened';
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.latestSHA = data.diff_head_sha;
this.canBeMerged = data.can_be_merged || false;
this.isMergeAllowed = data.mergeable || false;
this.mergeOngoing = data.merge_ongoing;
this.allowCollaboration = data.allow_collaboration;
......@@ -109,7 +97,6 @@ export default class MergeRequestStore {
// CI related
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isPipelinePassing =
this.ciStatus === 'success' || this.ciStatus === 'success-with-warnings';
this.isPipelineSkipped = this.ciStatus === 'skipped';
......@@ -134,11 +121,24 @@ export default class MergeRequestStore {
this.removeWIPPath = data.remove_wip_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergePath = data.merge_path;
this.canMerge = Boolean(data.merge_path);
this.mergeCommitPath = data.merged_commit_path;
this.canPushToSourceBranch = data.can_push_to_source_branch;
if (data.work_in_progress !== undefined) {
if (!window.gon?.features?.mergeRequestWidgetGraphql) {
this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
this.canBeMerged = data.can_be_merged || false;
this.canMerge = Boolean(data.merge_path);
this.commitsCount = data.commits_count;
this.branchMissing = data.branch_missing;
this.hasConflicts = data.has_conflicts;
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.mergeError = data.merge_error;
this.mergeStatus = data.merge_status;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.projectArchived = data.project_archived;
this.isSHAMismatch = this.sha !== data.diff_head_sha;
this.shouldBeRebased = Boolean(data.should_be_rebased);
this.workInProgress = data.work_in_progress;
}
......@@ -155,8 +155,27 @@ export default class MergeRequestStore {
this.setState();
}
setGraphqlData(data) {
this.workInProgress = data.workInProgress;
setGraphqlData(project) {
const { mergeRequest } = project;
const pipeline = mergeRequest.pipelines?.nodes?.[0];
this.projectArchived = project.archived;
this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
this.autoMergeEnabled = mergeRequest.autoMergeEnabled;
this.canBeMerged = mergeRequest.mergeStatus === 'can_be_merged';
this.canMerge = mergeRequest.userPermissions.canMerge;
this.ciStatus = pipeline?.status.toLowerCase();
this.commitsCount = mergeRequest.commitCount;
this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
this.hasConflicts = mergeRequest.conflicts;
this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false;
this.mergeError = mergeRequest.mergeError;
this.mergeStatus = mergeRequest.mergeStatus;
this.isPipelineFailed = this.ciStatus === 'failed' || this.ciStatus === 'canceled';
this.isSHAMismatch = this.sha !== mergeRequest.diffHeadSha;
this.shouldBeRebased = mergeRequest.shouldBeRebased;
this.workInProgress = mergeRequest.workInProgress;
this.setState();
}
......
......@@ -7,6 +7,8 @@ module Resolvers
alias_method :merge_request, :object
def resolve(**args)
return unless project
resolve_pipelines(project, args)
.merge(merge_request.all_pipelines)
end
......
......@@ -80,7 +80,7 @@ module Types
description: 'Error message due to a merge error'
field :allow_collaboration, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if members of the target project can push to the fork'
field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false,
field :should_be_rebased, GraphQL::BOOLEAN_TYPE, method: :should_be_rebased?, null: false, calls_gitaly: true,
description: 'Indicates if the merge request will be rebased'
field :rebase_commit_sha, GraphQL::STRING_TYPE, null: true,
description: 'Rebase commit SHA of the merge request'
......@@ -113,6 +113,7 @@ module Types
field :head_pipeline, Types::Ci::PipelineType, null: true, method: :actual_head_pipeline,
description: 'The pipeline running on the branch HEAD of the merge request'
field :pipelines, Types::Ci::PipelineType.connection_type,
null: true,
description: 'Pipelines for the merge request',
resolver: Resolvers::MergeRequestPipelinesResolver
......@@ -146,6 +147,10 @@ module Types
description: Types::TaskCompletionStatus.description
field :commit_count, GraphQL::INT_TYPE, null: true,
description: 'Number of commits in the merge request'
field :conflicts, GraphQL::BOOLEAN_TYPE, null: false, method: :cannot_be_merged?,
description: 'Indicates if the merge request has conflicts'
field :auto_merge_enabled, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates if auto merge is enabled for the merge request'
def diff_stats(path: nil)
stats = Array.wrap(object.diff_stats&.to_a)
......
......@@ -18,6 +18,10 @@ module Types
PERMISSION_FIELDS.each do |field_name|
permission_field field_name, method: :"can_#{field_name}?", calls_gitaly: true
end
permission_field :can_merge, calls_gitaly: true, resolve: -> (object, args, context) do
object.can_be_merged_by?(context[:current_user])
end
end
end
end
......@@ -9272,11 +9272,21 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
author: User
"""
Indicates if auto merge is enabled for the merge request
"""
autoMergeEnabled: Boolean!
"""
Number of commits in the merge request
"""
commitCount: Int
"""
Indicates if the merge request has conflicts
"""
conflicts: Boolean!
"""
Timestamp of when the merge request was created
"""
......@@ -9570,7 +9580,7 @@ type MergeRequest implements CurrentUserTodos & Noteable {
Filter pipelines by their status
"""
status: PipelineStatusEnum
): PipelineConnection!
): PipelineConnection
"""
Alias for target_project
......@@ -9827,6 +9837,11 @@ type MergeRequestPermissions {
"""
adminMergeRequest: Boolean!
"""
Indicates the user can perform `can_merge` on this resource
"""
canMerge: Boolean!
"""
Indicates the user can perform `cherry_pick_on_current_merge_request` on this resource
"""
......
......@@ -25712,6 +25712,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "autoMergeEnabled",
"description": "Indicates if auto merge is enabled for the merge request",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "commitCount",
"description": "Number of commits in the merge request",
......@@ -25726,6 +25744,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "conflicts",
"description": "Indicates if the merge request has conflicts",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createdAt",
"description": "Timestamp of when the merge request was created",
......@@ -26466,13 +26502,9 @@
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PipelineConnection",
"ofType": null
}
"kind": "OBJECT",
"name": "PipelineConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
......@@ -27304,6 +27336,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "canMerge",
"description": "Indicates the user can perform `can_merge` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "cherryPickOnCurrentMergeRequest",
"description": "Indicates the user can perform `cherry_pick_on_current_merge_request` on this resource",
......@@ -1385,7 +1385,9 @@ Autogenerated return type of MarkAsSpamSnippet
| `allowCollaboration` | Boolean | Indicates if members of the target project can push to the fork |
| `approved` | Boolean! | Indicates if the merge request has all the required approvals. Returns true if no required approvals are configured. |
| `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
| `commitCount` | Int | Number of commits in the merge request |
| `conflicts` | Boolean! | Indicates if the merge request has conflicts |
| `createdAt` | Time! | Timestamp of when the merge request was created |
| `defaultMergeCommitMessage` | String | Default merge commit message of the merge request |
| `description` | String | Description of the merge request (Markdown rendered as HTML for caching) |
......@@ -1456,6 +1458,7 @@ Check permissions for the current user on a merge request
| Name | Type | Description |
| --- | ---- | ---------- |
| `adminMergeRequest` | Boolean! | Indicates the user can perform `admin_merge_request` on this resource |
| `canMerge` | Boolean! | Indicates the user can perform `can_merge` on this resource |
| `cherryPickOnCurrentMergeRequest` | Boolean! | Indicates the user can perform `cherry_pick_on_current_merge_request` on this resource |
| `createNote` | Boolean! | Indicates the user can perform `create_note` on this resource |
| `pushToSourceBranch` | Boolean! | Indicates the user can perform `push_to_source_branch` on this resource |
......
......@@ -234,7 +234,7 @@ export default {
};
</script>
<template>
<div v-if="mr" class="mr-state-widget gl-mt-3">
<div v-if="isLoaded" class="mr-state-widget gl-mt-3">
<mr-widget-header :mr="mr" />
<mr-widget-suggest-pipeline
v-if="shouldSuggestPipelines"
......
......@@ -110,6 +110,7 @@ describe('ee merge request widget options', () => {
mock.onGet(VULNERABILITY_FEEDBACK_ENDPOINT).reply(200, []);
vm = mountComponent(Component, { mrData: gl.mrWidgetData });
vm.loading = false;
expect(
findSecurityWidget()
......
......@@ -29,4 +29,11 @@ RSpec.describe Resolvers::MergeRequestPipelinesResolver do
it 'resolves only MRs for the passed merge request' do
expect(resolve_pipelines).to contain_exactly(pipeline)
end
describe 'with archived project' do
let(:archived_project) { create(:project, :archived) }
let(:merge_request) { create(:merge_request, source_project: archived_project) }
it { expect(resolve_pipelines).not_to contain_exactly(pipeline) }
end
end
......@@ -27,6 +27,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
upvotes downvotes head_pipeline pipelines task_completion_status
milestone assignees participants subscribed labels discussion_locked time_estimate
total_time_spent reference author merged_at commit_count current_user_todos
conflicts auto_merge_enabled
]
if Gitlab.ee?
......
......@@ -7,7 +7,8 @@ RSpec.describe Types::PermissionTypes::MergeRequest do
expected_permissions = [
:read_merge_request, :admin_merge_request, :update_merge_request,
:create_note, :push_to_source_branch, :remove_source_branch,
:cherry_pick_on_current_merge_request, :revert_on_current_merge_request
:cherry_pick_on_current_merge_request, :revert_on_current_merge_request,
:can_merge
]
expect(described_class).to have_graphql_fields(expected_permissions)
......
......@@ -124,7 +124,8 @@ RSpec.describe 'getting merge request information nested in a project' do
'removeSourceBranch' => false,
'cherryPickOnCurrentMergeRequest' => false,
'revertOnCurrentMergeRequest' => false,
'updateMergeRequest' => false
'updateMergeRequest' => false,
'canMerge' => false
}
post_graphql(query, current_user: current_user)
......
......@@ -204,6 +204,10 @@ RSpec.configure do |config|
# unified diff lines works as expected
stub_feature_flags(unified_diff_lines: false)
# Merge request widget GraphQL requests are disabled in the tests
# for now whilst we migrate as much as we can over the GraphQL
stub_feature_flags(merge_request_widget_graphql: false)
enable_rugged = example.metadata[:enable_rugged].present?
# Disable Rugged features by default
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment