Commit 5b0a35eb authored by Mayra Cabrera's avatar Mayra Cabrera

Merge branch 'ph/235717/moveMRStateKeyDataToGraphql' into 'master'

Move core merge request widget state into GraphQL

Closes #235717

See merge request gitlab-org/gitlab!39838
parents e5213830 cf3477d2
......@@ -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
......@@ -9422,11 +9422,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
"""
......@@ -9720,7 +9730,7 @@ type MergeRequest implements CurrentUserTodos & Noteable {
Filter pipelines by their status
"""
status: PipelineStatusEnum
): PipelineConnection!
): PipelineConnection
"""
Alias for target_project
......@@ -9977,6 +9987,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
"""
......
......@@ -26149,6 +26149,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",
......@@ -26163,6 +26181,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",
......@@ -26903,13 +26939,9 @@
}
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PipelineConnection",
"ofType": null
}
"kind": "OBJECT",
"name": "PipelineConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
......@@ -27741,6 +27773,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",
......@@ -1417,7 +1417,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) |
......@@ -1488,6 +1490,7 @@ Check permissions for the current user on a merge request
| Field | 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