Commit 73803b08 authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'ph/235712/widgetConflictsToGraphql' into 'master'

Converts widget conflicts state data to GraphQL

See merge request gitlab-org/gitlab!48125
parents e8fb6df8 54bdd83e
<script>
import $ from 'jquery';
import { escape } from 'lodash';
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModalDirective, GlSkeletonLoader } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import StatusIcon from '../mr_widget_status_icon.vue';
import userPermissionsQuery from '../../queries/permissions.query.graphql';
import conflictsStateQuery from '../../queries/states/conflicts.query.graphql';
export default {
name: 'MRWidgetConflicts',
components: {
GlSkeletonLoader,
StatusIcon,
GlButton,
},
directives: {
GlModalDirective,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
apollo: {
userPermissions: {
query: userPermissionsQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: data => data.project.mergeRequest.userPermissions,
},
stateData: {
query: conflictsStateQuery,
skip() {
return !this.glFeatures.mergeRequestWidgetGraphql;
},
variables() {
return this.mergeRequestQueryVariables;
},
update: data => data.project.mergeRequest,
},
},
props: {
/* TODO: This is providing all store and service down when it
only needs a few props */
......@@ -24,21 +52,72 @@ export default {
default: () => ({}),
},
},
data() {
return {
userPermissions: {},
stateData: {},
};
},
computed: {
isLoading() {
return (
this.glFeatures.mergeRequestWidgetGraphql &&
this.$apollo.queries.userPermissions.loading &&
this.$apollo.queries.stateData.loading
);
},
canPushToSourceBranch() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.userPermissions.pushToSourceBranch;
}
return this.mr.canPushToSourceBranch;
},
canMerge() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.userPermissions.canMerge;
}
return this.mr.canMerge;
},
shouldBeRebased() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.stateData.shouldBeRebased;
}
return this.mr.shouldBeRebased;
},
sourceBranchProtected() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.stateData.sourceBranchProtected;
}
return this.mr.sourceBranchProtected;
},
popoverTitle() {
return s__(
'mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected.',
);
},
showResolveButton() {
return this.mr.conflictResolutionPath && this.mr.canPushToSourceBranch;
return this.mr.conflictResolutionPath && this.canPushToSourceBranch;
},
showPopover() {
return this.showResolveButton && this.mr.sourceBranchProtected;
return this.showResolveButton && this.sourceBranchProtected;
},
},
mounted() {
if (this.showPopover) {
watch: {
showPopover: {
handler(newVal) {
if (newVal) {
this.$nextTick(this.initPopover);
}
},
immediate: true,
},
},
methods: {
initPopover() {
const $el = $(this.$refs.popover);
$el
......@@ -68,7 +147,7 @@ export default {
.on('show.bs.popover', () => {
window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
});
}
},
},
};
</script>
......@@ -76,34 +155,41 @@ export default {
<div class="mr-widget-body media">
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
<span v-if="mr.shouldBeRebased" class="bold">
<div v-if="isLoading" class="gl-ml-4 gl-w-full mr-conflict-loader">
<gl-skeleton-loader :width="334" :height="30">
<rect x="0" y="7" width="150" height="16" rx="4" />
<rect x="158" y="7" width="84" height="16" rx="4" />
<rect x="250" y="7" width="84" height="16" rx="4" />
</gl-skeleton-loader>
</div>
<div v-else class="media-body space-children">
<span v-if="shouldBeRebased" class="bold">
{{
s__(`mrWidget|Fast-forward merge is not possible.
To merge this request, first rebase locally.`)
To merge this request, first rebase locally.`)
}}
</span>
<template v-else>
<span class="bold">
{{ s__('mrWidget|There are merge conflicts') }}<span v-if="!mr.canMerge">.</span>
<span v-if="!mr.canMerge">
{{ s__('mrWidget|There are merge conflicts') }}<span v-if="!canMerge">.</span>
<span v-if="!canMerge">
{{
s__(`mrWidget|Resolve these conflicts or ask someone
with write access to this repository to merge it locally`)
with write access to this repository to merge it locally`)
}}
</span>
</span>
<span v-if="showResolveButton" ref="popover">
<gl-button
:href="mr.conflictResolutionPath"
:disabled="mr.sourceBranchProtected"
:href="!sourceBranchProtected && mr.conflictResolutionPath"
:disabled="sourceBranchProtected"
class="js-resolve-conflicts-button"
>
{{ s__('mrWidget|Resolve conflicts') }}
</gl-button>
</span>
<gl-button
v-if="mr.canMerge"
v-if="canMerge"
v-gl-modal-directive="'modal-merge-info'"
class="js-merge-locally-button"
>
......
query userPermissionsQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
userPermissions {
canMerge
pushToSourceBranch
}
}
}
}
query workInProgressQuery($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
shouldBeRebased
sourceBranchProtected
}
}
}
......@@ -1039,3 +1039,11 @@ $mr-widget-min-height: 69px;
.diff-file-row.is-active {
background-color: $gray-50;
}
.mr-conflict-loader {
max-width: 334px;
> svg {
vertical-align: middle;
}
}
......@@ -49,6 +49,8 @@ module Types
description: 'ID of the merge request target project'
field :source_branch, GraphQL::STRING_TYPE, null: false,
description: 'Source branch of the merge request'
field :source_branch_protected, GraphQL::BOOLEAN_TYPE, null: false, calls_gitaly: true,
description: 'Indicates if the source branch is protected'
field :target_branch, GraphQL::STRING_TYPE, null: false,
description: 'Target branch of the merge request'
field :work_in_progress, GraphQL::BOOLEAN_TYPE, method: :work_in_progress?, null: false,
......@@ -194,6 +196,10 @@ module Types
def commit_count
object&.metrics&.commits_count
end
def source_branch_protected
object.source_project.present? && ProtectedBranch.protected?(object.source_project, object.source_branch)
end
end
end
Types::MergeRequestType.prepend_if_ee('::EE::Types::MergeRequestType')
......@@ -12780,6 +12780,11 @@ type MergeRequest implements CurrentUserTodos & Noteable {
"""
sourceBranchExists: Boolean!
"""
Indicates if the source branch is protected
"""
sourceBranchProtected: Boolean!
"""
Source project of the merge request
"""
......
......@@ -35042,6 +35042,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sourceBranchProtected",
"description": "Indicates if the source branch is protected",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "sourceProject",
"description": "Source project of the merge request",
......@@ -1950,6 +1950,7 @@ Autogenerated return type of MarkAsSpamSnippet.
| `shouldRemoveSourceBranch` | Boolean | Indicates if the source branch of the merge request will be deleted after merge |
| `sourceBranch` | String! | Source branch of the merge request |
| `sourceBranchExists` | Boolean! | Indicates if the source branch of the merge request exists |
| `sourceBranchProtected` | Boolean! | Indicates if the source branch is protected |
| `sourceProject` | Project | Source project of the merge request |
| `sourceProjectId` | Int | ID of the merge request source project |
| `state` | MergeRequestState! | State of the merge request |
......
......@@ -6,6 +6,7 @@ import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_
describe('MRWidgetConflicts', () => {
let vm;
let mergeRequestWidgetGraphql = null;
const path = '/conflicts';
function createComponent(propsData = {}) {
......@@ -13,7 +14,35 @@ describe('MRWidgetConflicts', () => {
vm = shallowMount(localVue.extend(ConflictsComponent), {
propsData,
provide: {
glFeatures: {
mergeRequestWidgetGraphql,
},
},
mocks: {
$apollo: {
queries: {
userPermissions: { loading: false },
stateData: { loading: false },
},
},
},
});
if (mergeRequestWidgetGraphql) {
vm.setData({
userPermissions: {
canMerge: propsData.mr.canMerge,
pushToSourceBranch: propsData.mr.canPushToSourceBranch,
},
stateData: {
shouldBeRebased: propsData.mr.shouldBeRebased,
sourceBranchProtected: propsData.mr.sourceBranchProtected,
},
});
}
return vm.vm.$nextTick();
}
beforeEach(() => {
......@@ -21,206 +50,215 @@ describe('MRWidgetConflicts', () => {
});
afterEach(() => {
mergeRequestWidgetGraphql = null;
vm.destroy();
});
// There are two permissions we need to consider:
//
// 1. Is the user allowed to merge to the target branch?
// 2. Is the user allowed to push to the source branch?
//
// This yields 4 possible permutations that we need to test, and
// we test them below. A user who can push to the source
// branch should be allowed to resolve conflicts. This is
// consistent with what the backend does.
describe('when allowed to merge but not allowed to push to source branch', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: false,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
[false, true].forEach(featureEnabled => {
describe(`with GraphQL feature flag ${featureEnabled ? 'enabled' : 'disabled'}`, () => {
beforeEach(() => {
mergeRequestWidgetGraphql = featureEnabled;
});
});
it('should tell you about conflicts without bothering other people', () => {
expect(vm.text()).toContain('There are merge conflicts');
expect(vm.text()).not.toContain('ask someone with write access');
});
it('should not allow you to resolve the conflicts', () => {
expect(vm.text()).not.toContain('Resolve conflicts');
});
it('should have merge buttons', () => {
const mergeLocallyButton = vm.find('.js-merge-locally-button');
expect(mergeLocallyButton.text()).toContain('Merge locally');
});
});
describe('when not allowed to merge but allowed to push to source branch', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: true,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
});
});
it('should tell you about conflicts', () => {
expect(vm.text()).toContain('There are merge conflicts');
expect(vm.text()).toContain('ask someone with write access');
});
it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.find('.js-resolve-conflicts-button');
expect(resolveButton.text()).toContain('Resolve conflicts');
expect(resolveButton.attributes('href')).toEqual(path);
});
it('should not have merge buttons', () => {
expect(vm.text()).not.toContain('Merge locally');
});
});
describe('when allowed to merge and push to source branch', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
// There are two permissions we need to consider:
//
// 1. Is the user allowed to merge to the target branch?
// 2. Is the user allowed to push to the source branch?
//
// This yields 4 possible permutations that we need to test, and
// we test them below. A user who can push to the source
// branch should be allowed to resolve conflicts. This is
// consistent with what the backend does.
describe('when allowed to merge but not allowed to push to source branch', () => {
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: false,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
});
});
it('should tell you about conflicts without bothering other people', () => {
expect(vm.text()).toContain('There are merge conflicts');
expect(vm.text()).not.toContain('ask someone with write access');
});
it('should not allow you to resolve the conflicts', () => {
expect(vm.text()).not.toContain('Resolve conflicts');
});
it('should have merge buttons', () => {
const mergeLocallyButton = vm.find('.js-merge-locally-button');
expect(mergeLocallyButton.text()).toContain('Merge locally');
});
});
});
it('should tell you about conflicts without bothering other people', () => {
expect(vm.text()).toContain('There are merge conflicts');
expect(vm.text()).not.toContain('ask someone with write access');
});
it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.find('.js-resolve-conflicts-button');
expect(resolveButton.text()).toContain('Resolve conflicts');
expect(resolveButton.attributes('href')).toEqual(path);
});
it('should have merge buttons', () => {
const mergeLocallyButton = vm.find('.js-merge-locally-button');
expect(mergeLocallyButton.text()).toContain('Merge locally');
});
});
describe('when user does not have permission to push to source branch', () => {
it('should show proper message', () => {
createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: false,
conflictsDocsPath: '',
},
describe('when not allowed to merge but allowed to push to source branch', () => {
beforeEach(async () => {
await createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: true,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
});
});
it('should tell you about conflicts', () => {
expect(vm.text()).toContain('There are merge conflicts');
expect(vm.text()).toContain('ask someone with write access');
});
it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.find('.js-resolve-conflicts-button');
expect(resolveButton.text()).toContain('Resolve conflicts');
expect(resolveButton.attributes('href')).toEqual(path);
});
it('should not have merge buttons', () => {
expect(vm.text()).not.toContain('Merge locally');
});
});
expect(
vm
.text()
.trim()
.replace(/\s\s+/g, ' '),
).toContain('ask someone with write access');
});
it('should not have action buttons', () => {
createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: false,
conflictsDocsPath: '',
},
describe('when allowed to merge and push to source branch', () => {
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
});
});
it('should tell you about conflicts without bothering other people', () => {
expect(vm.text()).toContain('There are merge conflicts');
expect(vm.text()).not.toContain('ask someone with write access');
});
it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.find('.js-resolve-conflicts-button');
expect(resolveButton.text()).toContain('Resolve conflicts');
expect(resolveButton.attributes('href')).toEqual(path);
});
it('should have merge buttons', () => {
const mergeLocallyButton = vm.find('.js-merge-locally-button');
expect(mergeLocallyButton.text()).toContain('Merge locally');
});
});
expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false);
expect(vm.find('.js-merge-locally-button').exists()).toBe(false);
});
it('should not have resolve button when no conflict resolution path', () => {
createComponent({
mr: {
canMerge: true,
conflictResolutionPath: null,
conflictsDocsPath: '',
},
describe('when user does not have permission to push to source branch', () => {
it('should show proper message', async () => {
await createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: false,
conflictsDocsPath: '',
},
});
expect(
vm
.text()
.trim()
.replace(/\s\s+/g, ' '),
).toContain('ask someone with write access');
});
it('should not have action buttons', async () => {
await createComponent({
mr: {
canMerge: false,
canPushToSourceBranch: false,
conflictsDocsPath: '',
},
});
expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false);
expect(vm.find('.js-merge-locally-button').exists()).toBe(false);
});
it('should not have resolve button when no conflict resolution path', async () => {
await createComponent({
mr: {
canMerge: true,
conflictResolutionPath: null,
conflictsDocsPath: '',
},
});
expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false);
});
});
expect(vm.find('.js-resolve-conflicts-button').exists()).toBe(false);
});
});
describe('when fast-forward or semi-linear merge enabled', () => {
it('should tell you to rebase locally', () => {
createComponent({
mr: {
shouldBeRebased: true,
conflictsDocsPath: '',
},
describe('when fast-forward or semi-linear merge enabled', () => {
it('should tell you to rebase locally', async () => {
await createComponent({
mr: {
shouldBeRebased: true,
conflictsDocsPath: '',
},
});
expect(removeBreakLine(vm.text()).trim()).toContain(
'Fast-forward merge is not possible. To merge this request, first rebase locally.',
);
});
});
expect(removeBreakLine(vm.text()).trim()).toContain(
'Fast-forward merge is not possible. To merge this request, first rebase locally.',
);
});
});
describe('when source branch protected', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: TEST_HOST,
sourceBranchProtected: true,
conflictsDocsPath: '',
},
describe('when source branch protected', () => {
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: TEST_HOST,
sourceBranchProtected: true,
conflictsDocsPath: '',
},
});
});
it('sets resolve button as disabled', () => {
expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('true');
});
it('renders popover', () => {
expect($.fn.popover).toHaveBeenCalled();
});
});
});
it('sets resolve button as disabled', () => {
expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('true');
});
it('renders popover', () => {
expect($.fn.popover).toHaveBeenCalled();
});
});
describe('when source branch not protected', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: TEST_HOST,
sourceBranchProtected: false,
conflictsDocsPath: '',
},
describe('when source branch not protected', () => {
beforeEach(async () => {
await createComponent({
mr: {
canMerge: true,
canPushToSourceBranch: true,
conflictResolutionPath: TEST_HOST,
sourceBranchProtected: false,
conflictsDocsPath: '',
},
});
});
it('sets resolve button as disabled', () => {
expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe(undefined);
});
it('renders popover', () => {
expect($.fn.popover).not.toHaveBeenCalled();
});
});
});
it('sets resolve button as disabled', () => {
expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe(undefined);
});
it('renders popover', () => {
expect($.fn.popover).not.toHaveBeenCalled();
});
});
});
......@@ -27,7 +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 approved_by
conflicts auto_merge_enabled approved_by source_branch_protected
]
if Gitlab.ee?
......
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