Commit 5a827e0c authored by Nick Thomas's avatar Nick Thomas

Merge branch 'mr-widget-conflicts-protected-branch' into 'master'

Disable resolve conflicts for protected branches

Closes #53463

See merge request gitlab-org/gitlab-ce!23842
parents 14896994 aae6d174
<script> <script>
import statusIcon from '../mr_widget_status_icon.vue'; import $ from 'jquery';
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
import StatusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetConflicts', name: 'MRWidgetConflicts',
components: { components: {
statusIcon, StatusIcon,
}, },
props: { props: {
/* TODO: This is providing all store and service down when it /* TODO: This is providing all store and service down when it
...@@ -15,6 +19,52 @@ export default { ...@@ -15,6 +19,52 @@ export default {
default: () => ({}), default: () => ({}),
}, },
}, },
computed: {
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.canMerge;
},
showPopover() {
return this.showResolveButton && this.mr.sourceBranchProtected;
},
},
mounted() {
if (this.showPopover) {
const $el = $(this.$refs.popover);
$el
.popover({
html: true,
trigger: 'focus',
container: 'body',
placement: 'top',
template:
'<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>',
title: 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.',
),
content: sprintf(
s__('mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}'),
{
link_start: `<a href="${_.escape(
this.mr.conflictsDocsPath,
)}" target="_blank" rel="noopener noreferrer">`,
link_end: '</a>',
},
false,
),
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave(300))
.on('show.bs.popover', () => {
window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
});
}
},
}; };
</script> </script>
<template> <template>
...@@ -38,13 +88,15 @@ To merge this request, first rebase locally.`) ...@@ -38,13 +88,15 @@ To merge this request, first rebase locally.`)
}} }}
</span> </span>
</span> </span>
<a <span v-if="showResolveButton" ref="popover">
v-if="mr.canMerge && mr.conflictResolutionPath" <a
:href="mr.conflictResolutionPath" :href="mr.conflictResolutionPath"
class="js-resolve-conflicts-button btn btn-default btn-sm" :disabled="mr.sourceBranchProtected"
> class="js-resolve-conflicts-button btn btn-default btn-sm"
{{ s__('mrWidget|Resolve conflicts') }} >
</a> {{ s__('mrWidget|Resolve conflicts') }}
</a>
</span>
<button <button
v-if="mr.canMerge" v-if="mr.canMerge"
class="js-merge-locally-button btn btn-default btn-sm" class="js-merge-locally-button btn btn-default btn-sm"
......
...@@ -29,6 +29,8 @@ export default class MergeRequestStore { ...@@ -29,6 +29,8 @@ export default class MergeRequestStore {
this.title = data.title; this.title = data.title;
this.targetBranch = data.target_branch; this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch; this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeStatus = data.merge_status; this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message; this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha; this.shortMergeCommitSha = data.short_merge_commit_sha;
......
...@@ -189,6 +189,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated ...@@ -189,6 +189,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
merge_request.subscribed?(current_user, merge_request.target_project) merge_request.subscribed?(current_user, merge_request.target_project)
end end
def conflicts_docs_path
help_page_path('user/project/merge_requests/resolve_conflicts.md')
end
private private
def cached_can_be_reverted? def cached_can_be_reverted?
......
...@@ -11,6 +11,9 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -11,6 +11,9 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_user_id expose :merge_user_id
expose :merge_when_pipeline_succeeds expose :merge_when_pipeline_succeeds
expose :source_branch expose :source_branch
expose :source_branch_protected do |merge_request|
merge_request.source_project.present? && ProtectedBranch.protected?(merge_request.source_project, merge_request.source_branch)
end
expose :source_project_id expose :source_project_id
expose :source_project_full_path do |merge_request| expose :source_project_full_path do |merge_request|
merge_request.source_project&.full_path merge_request.source_project&.full_path
...@@ -240,6 +243,10 @@ class MergeRequestWidgetEntity < IssuableEntity ...@@ -240,6 +243,10 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :supports_suggestion?, as: :can_receive_suggestion expose :supports_suggestion?, as: :can_receive_suggestion
expose :conflicts_docs_path do |merge_request|
presenter(merge_request).conflicts_docs_path
end
private private
delegate :current_user, to: :request delegate :current_user, to: :request
......
...@@ -8076,6 +8076,9 @@ msgstr[1] "" ...@@ -8076,6 +8076,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch" msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
msgstr "" msgstr ""
msgid "mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}"
msgstr ""
msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB" msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
msgstr "" msgstr ""
...@@ -8235,6 +8238,9 @@ msgstr "" ...@@ -8235,6 +8238,9 @@ msgstr ""
msgid "mrWidget|There are unresolved discussions. Please resolve these discussions" msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
msgstr "" msgstr ""
msgid "mrWidget|This feature merges changes from the target branch to the source branch. You cannot use this feature since the source branch is protected."
msgstr ""
msgid "mrWidget|This merge request failed to be merged automatically" msgid "mrWidget|This merge request failed to be merged automatically"
msgstr "" msgstr ""
......
...@@ -120,7 +120,9 @@ ...@@ -120,7 +120,9 @@
"rebase_path": { "type": ["string", "null"] }, "rebase_path": { "type": ["string", "null"] },
"squash": { "type": "boolean" }, "squash": { "type": "boolean" },
"test_reports_path": { "type": ["string", "null"] }, "test_reports_path": { "type": ["string", "null"] },
"can_receive_suggestion": { "type": "boolean" } "can_receive_suggestion": { "type": "boolean" },
"source_branch_protected": { "type": "boolean" },
"conflicts_docs_path": { "type": ["string", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
} }
import Vue from 'vue'; import $ from 'jquery';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import { removeBreakLine } from 'spec/helpers/vue_component_helper'; import { removeBreakLine } from 'spec/helpers/vue_component_helper';
describe('MRWidgetConflicts', () => { describe('MRWidgetConflicts', () => {
let Component;
let vm; let vm;
const path = '/conflicts'; const path = '/conflicts';
function createComponent(propsData = {}) {
const localVue = createLocalVue();
vm = shallowMount(localVue.extend(ConflictsComponent), {
propsData,
});
}
beforeEach(() => { beforeEach(() => {
Component = Vue.extend(conflictsComponent); spyOn($.fn, 'popover').and.callThrough();
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); vm.destroy();
}); });
describe('when allowed to merge', () => { describe('when allowed to merge', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
canMerge: true, canMerge: true,
conflictResolutionPath: path, conflictResolutionPath: path,
conflictsDocsPath: '',
}, },
}); });
}); });
it('should tell you about conflicts without bothering other people', () => { it('should tell you about conflicts without bothering other people', () => {
expect(vm.$el.textContent).toContain('There are merge conflicts'); expect(vm.text()).toContain('There are merge conflicts');
expect(vm.$el.textContent).not.toContain('ask someone with write access'); expect(vm.text()).not.toContain('ask someone with write access');
}); });
it('should allow you to resolve the conflicts', () => { it('should allow you to resolve the conflicts', () => {
const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button'); const resolveButton = vm.find('.js-resolve-conflicts-button');
expect(resolveButton.textContent).toContain('Resolve conflicts'); expect(resolveButton.text()).toContain('Resolve conflicts');
expect(resolveButton.getAttribute('href')).toEqual(path); expect(resolveButton.attributes('href')).toEqual(path);
}); });
it('should have merge buttons', () => { it('should have merge buttons', () => {
const mergeButton = vm.$el.querySelector('.js-disabled-merge-button'); const mergeLocallyButton = vm.find('.js-merge-locally-button');
const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
expect(mergeButton.textContent).toContain('Merge'); expect(mergeLocallyButton.text()).toContain('Merge locally');
expect(mergeButton.disabled).toBeTruthy();
expect(mergeButton.classList.contains('btn-success')).toEqual(true);
expect(mergeLocallyButton.textContent).toContain('Merge locally');
}); });
}); });
describe('when user does not have permission to merge', () => { describe('when user does not have permission to merge', () => {
beforeEach(() => { it('should show proper message', () => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
canMerge: false, canMerge: false,
conflictsDocsPath: '',
}, },
}); });
});
it('should show proper message', () => { expect(
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain( vm
'ask someone with write access', .text()
); .trim()
.replace(/\s\s+/g, ' '),
).toContain('ask someone with write access');
}); });
it('should not have action buttons', () => { it('should not have action buttons', () => {
expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined(); createComponent({
expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull(); mr: {
expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull(); canMerge: false,
conflictsDocsPath: '',
},
});
expect(vm.contains('.js-resolve-conflicts-button')).toBe(false);
expect(vm.contains('.js-merge-locally-button')).toBe(false);
});
it('should not have resolve button when no conflict resolution path', () => {
createComponent({
mr: {
canMerge: true,
conflictResolutionPath: null,
conflictsDocsPath: '',
},
});
expect(vm.contains('.js-resolve-conflicts-button')).toBe(false);
}); });
}); });
describe('when fast-forward or semi-linear merge enabled', () => { describe('when fast-forward or semi-linear merge enabled', () => {
beforeEach(() => { it('should tell you to rebase locally', () => {
vm = mountComponent(Component, { createComponent({
mr: { mr: {
shouldBeRebased: true, shouldBeRebased: true,
conflictsDocsPath: '',
}, },
}); });
});
it('should tell you to rebase locally', () => { expect(removeBreakLine(vm.text()).trim()).toContain(
expect(removeBreakLine(vm.$el.textContent).trim()).toContain(
'Fast-forward merge is not possible. To merge this request, first rebase locally.', 'Fast-forward merge is not possible. To merge this request, first rebase locally.',
); );
}); });
}); });
describe('when source branch protected', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: true,
conflictResolutionPath: gl.TEST_HOST,
sourceBranchProtected: true,
conflictsDocsPath: '',
},
});
});
it('sets resolve button as disabled', () => {
expect(vm.find('.js-resolve-conflicts-button').attributes('disabled')).toBe('disabled');
});
it('renders popover', () => {
expect($.fn.popover).toHaveBeenCalled();
});
});
describe('when source branch not protected', () => {
beforeEach(() => {
createComponent({
mr: {
canMerge: true,
conflictResolutionPath: gl.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();
});
});
}); });
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