Commit c46476c0 authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'ph/235715/autoMergeToGraphql' into 'master'

Moves the auto merge state to GraphQL

See merge request gitlab-org/gitlab!50980
parents ff1e3889 44926ce2
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../mr_widget_author.vue';
import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
import { __ } from '~/locale';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
name: 'MRWidgetAutoMergeEnabled',
apollo: {
state: {
query: autoMergeEnabledQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest,
},
},
components: {
MrWidgetAuthor,
statusIcon,
GlLoadingIcon,
GlSkeletonLoader,
},
mixins: [autoMergeMixin],
mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
mr: {
type: Object,
......@@ -30,20 +46,47 @@ export default {
},
data() {
return {
state: {},
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
loading() {
return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
},
mergeUser() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.mergeUser;
}
return this.mr.setToAutoMergeBy;
},
targetBranch() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).targetBranch;
},
shouldRemoveSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.shouldRemoveSourceBranch || this.state.forceRemoveSourceBranch;
}
return this.mr.shouldRemoveSourceBranch;
},
autoMergeStrategy() {
return (this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr).autoMergeStrategy;
},
canRemoveSourceBranch() {
const {
shouldRemoveSourceBranch,
canRemoveSourceBranch,
mergeUserId,
currentUserId,
} = this.mr;
const { currentUserId } = this.mr;
const mergeUserId = this.glFeatures.mergeRequestWidgetGraphql
? this.state.mergeUser?.id
: this.mr.mergeUserId;
const canRemoveSourceBranch = this.glFeatures.mergeRequestWidgetGraphql
? this.state.userPermissions.removeSourceBranch
: this.mr.canRemoveSourceBranch;
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
return (
!this.shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId
);
},
},
methods: {
......@@ -63,7 +106,7 @@ export default {
removeSourceBranch() {
const options = {
sha: this.mr.sha,
auto_merge_strategy: this.mr.autoMergeStrategy,
auto_merge_strategy: this.autoMergeStrategy,
should_remove_source_branch: true,
};
......@@ -86,49 +129,64 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<h4 class="d-flex align-items-start">
<span class="gl-mr-3">
<span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span>
<mr-widget-author :author="mr.setToAutoMergeBy" />
<span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
</span>
<a
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ mr.targetBranch }}</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="d-flex align-items-start">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<div v-if="loading" class="gl-w-full mr-conflict-loader">
<gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="3" width="24" height="24" rx="4" />
<rect x="32" y="7" width="150" height="16" rx="4" />
<rect x="190" y="7" width="144" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else>
<status-icon status="success" />
<div class="media-body">
<h4 class="gl-display-flex">
<span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{
statusTextBeforeAuthor
}}</span>
<mr-widget-author :author="mergeUser" />
<span class="js-status-text-after-author" data-testid="afterStatusText">{{
statusTextAfterAuthor
}}</span>
</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
@click.prevent="removeSourceBranch"
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
<gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</p>
</section>
</div>
</h4>
<section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a>
</p>
<p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
data-testid="removeSourceBranchButton"
@click.prevent="removeSourceBranch"
>
<gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
</a>
</p>
</section>
</div>
</template>
</div>
</template>
<script>
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import eventHub from '../../event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
import autoMergeFailedQuery from '../../queries/states/auto_merge_failed.query.graphql';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
export default {
name: 'MRWidgetAutoMergeFailed',
......@@ -10,6 +13,19 @@ export default {
GlLoadingIcon,
GlButton,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
mergeError: {
query: autoMergeFailedQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: (data) => data.project?.mergeRequest?.mergeError,
},
},
props: {
mr: {
type: Object,
......@@ -18,6 +34,7 @@ export default {
},
data() {
return {
mergeError: this.glFeatures.mergeRequestWidgetGraphql ? null : this.mr.mergeError,
isRefreshing: false,
};
},
......@@ -36,7 +53,7 @@ export default {
<status-icon status="warning" />
<div class="media-body space-children gl-display-flex gl-flex-wrap gl-align-items-center">
<span class="bold">
<template v-if="mr.mergeError">{{ mr.mergeError }}</template>
<template v-if="mergeError">{{ mergeError }}</template>
{{ s__('mrWidget|This merge request failed to be merged automatically') }}
</span>
<gl-button
......
fragment autoMergeEnabled on MergeRequest {
autoMergeStrategy
mergeUser {
name
username
webUrl
avatarUrl
}
targetBranch
shouldRemoveSourceBranch
forceRemoveSourceBranch
userPermissions {
removeSourceBranch
}
}
#import "./auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
mergeTrainsCount
}
}
}
query autoMergeFailedQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
mergeError
}
}
}
......@@ -175,6 +175,10 @@ module Types
calls_gitaly: true, description: 'Merge request commits excluding merge commits'
field :security_auto_fix, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if the merge request is created by @GitLab-Security-Bot.'
field :auto_merge_strategy, GraphQL::STRING_TYPE, null: true,
description: 'Selected auto merge strategy'
field :merge_user, Types::UserType, null: true,
description: 'User who merged this merge request'
def approved_by
object.approved_by_users
......
......@@ -13836,6 +13836,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
autoMergeEnabled: Boolean!
"""
Selected auto merge strategy
"""
autoMergeStrategy: String
"""
Array of available auto merge strategies
"""
......@@ -14075,6 +14080,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
mergeTrainsCount: Int
"""
User who merged this merge request
"""
mergeUser: User
"""
Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)
"""
......
......@@ -37994,6 +37994,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "autoMergeStrategy",
"description": "Selected auto merge strategy",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "availableAutoMergeStrategies",
"description": "Array of available auto merge strategies",
......@@ -38645,6 +38659,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeUser",
"description": "User who merged this merge request",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "User",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeWhenPipelineSucceeds",
"description": "Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS)",
......@@ -2095,6 +2095,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `assignees` | UserConnection | Assignees of the merge request |
| `author` | User | User who created this merge request |
| `autoMergeEnabled` | Boolean! | Indicates if auto merge is enabled for the merge request |
| `autoMergeStrategy` | String | Selected auto merge strategy |
| `availableAutoMergeStrategies` | String! => Array | Array of available auto merge strategies |
| `commitCount` | Int | Number of commits in the merge request |
| `commitsWithoutMergeCommits` | CommitConnection | Merge request commits excluding merge commits |
......@@ -2125,6 +2126,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `mergeOngoing` | Boolean! | Indicates if a merge is currently occurring |
| `mergeStatus` | String | Status of the merge request |
| `mergeTrainsCount` | Int | |
| `mergeUser` | User | User who merged this merge request |
| `mergeWhenPipelineSucceeds` | Boolean | Indicates if the merge has been set to be merged when its pipeline succeeds (MWPS) |
| `mergeable` | Boolean! | Indicates if the merge request is mergeable |
| `mergeableDiscussionsState` | Boolean | Indicates if all discussions in the merge request have been resolved, allowing the merge request to be merged |
......
......@@ -8,28 +8,27 @@ import { s__ } from '~/locale';
export default {
computed: {
statusTextBeforeAuthor() {
if (this.mr.autoMergeStrategy === MT_MERGE_STRATEGY) {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Added to the merge train by');
}
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
if (this.mr.autoMergeStrategy === MTWPS_MERGE_STRATEGY && this.mr.mergeTrainsCount === 0) {
const { mergeTrainsCount } = this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount === 0) {
return s__('mrWidget|to start a merge train when the pipeline succeeds');
} else if (
this.mr.autoMergeStrategy === MTWPS_MERGE_STRATEGY &&
this.mr.mergeTrainsCount !== 0
) {
} else if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount !== 0) {
return s__('mrWidget|to be added to the merge train when the pipeline succeeds');
} else if (this.mr.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
} else if (this.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
}
return '';
},
cancelButtonText() {
if (this.mr.autoMergeStrategy === MT_MERGE_STRATEGY) {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Remove from merge train');
}
......
#import "~/vue_merge_request_widget/queries/states/auto_merge_enabled.fragment.graphql"
query autoMergeEnabledQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
...autoMergeEnabled
}
}
}
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have correct elements 1`] = `
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
/>
<div
class="media-body"
>
<h4
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
>
<!---->
Cancel automatic merge
</a>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
<span
class="gl-mr-3"
>
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
>
<!---->
Delete source branch
</a>
</p>
</section>
</div>
</div>
`;
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
import AutoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
......@@ -8,43 +9,60 @@ describe('MRWidgetAutoMergeFailed', () => {
const mergeError = 'This is the merge error';
const findButton = () => wrapper.find(GlButton);
const createComponent = (props = {}) => {
const createComponent = (props = {}, mergeRequestWidgetGraphql = false) => {
wrapper = shallowMount(AutoMergeFailedComponent, {
propsData: { ...props },
});
};
data() {
if (mergeRequestWidgetGraphql) {
return { mergeError: props.mr?.mergeError };
}
beforeEach(() => {
createComponent({
mr: { mergeError },
return {};
},
provide: {
glFeatures: { mergeRequestWidgetGraphql },
},
});
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders failed message', () => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
});
[true, false].forEach((mergeRequestWidgetGraphql) => {
describe(`when graphql is ${mergeRequestWidgetGraphql ? 'enabled' : 'dislabed'}`, () => {
beforeEach(() => {
createComponent(
{
mr: { mergeError },
},
mergeRequestWidgetGraphql,
);
});
it('renders merge error provided', () => {
expect(wrapper.text()).toContain(mergeError);
});
it('renders failed message', () => {
expect(wrapper.text()).toContain('This merge request failed to be merged automatically');
});
it('render refresh button', () => {
expect(findButton().text()).toEqual('Refresh');
});
it('renders merge error provided', () => {
expect(wrapper.text()).toContain(mergeError);
});
it('render refresh button', () => {
expect(findButton().text()).toBe('Refresh');
});
it('emits event and shows loading icon when button is clicked', async () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
it('emits event and shows loading icon when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findButton().vm.$emit('click');
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
expect(eventHub.$emit.mock.calls[0][0]).toBe('MRWidgetUpdateRequested');
await nextTick();
return wrapper.vm.$nextTick(() => {
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(findButton().attributes('disabled')).toBe('true');
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
});
});
});
......@@ -30,6 +30,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'] do
conflicts auto_merge_enabled approved_by source_branch_protected
default_merge_commit_message_with_description squash_on_merge available_auto_merge_strategies
has_ci mergeable commits_without_merge_commits squash security_auto_fix default_squash_commit_message
auto_merge_strategy merge_user
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
......
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