Skip to content
Snippets Groups Projects
Unverified Commit aae6d174 authored by Phil Hughes's avatar Phil Hughes
Browse files

Disable resolve conflicts for protected branches

parent 301315f3
No related branches found
No related tags found
No related merge requests found
<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 {
name: 'MRWidgetConflicts',
components: {
statusIcon,
StatusIcon,
},
props: {
/* TODO: This is providing all store and service down when it
Loading
Loading
@@ -15,6 +19,52 @@ export 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>
<template>
Loading
Loading
@@ -38,13 +88,15 @@ To merge this request, first rebase locally.`)
}}
</span>
</span>
<a
v-if="mr.canMerge && mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
class="js-resolve-conflicts-button btn btn-default btn-sm"
>
{{ s__('mrWidget|Resolve conflicts') }}
</a>
<span v-if="showResolveButton" ref="popover">
<a
:href="mr.conflictResolutionPath"
:disabled="mr.sourceBranchProtected"
class="js-resolve-conflicts-button btn btn-default btn-sm"
>
{{ s__('mrWidget|Resolve conflicts') }}
</a>
</span>
<button
v-if="mr.canMerge"
class="js-merge-locally-button btn btn-default btn-sm"
Loading
Loading
Loading
Loading
@@ -29,6 +29,8 @@ export default class MergeRequestStore {
this.title = data.title;
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
this.sourceBranchProtected = data.source_branch_protected;
this.conflictsDocsPath = data.conflicts_docs_path;
this.mergeStatus = data.merge_status;
this.commitMessage = data.merge_commit_message;
this.shortMergeCommitSha = data.short_merge_commit_sha;
Loading
Loading
Loading
Loading
@@ -189,6 +189,10 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
merge_request.subscribed?(current_user, merge_request.target_project)
end
 
def conflicts_docs_path
help_page_path('user/project/merge_requests/resolve_conflicts.md')
end
private
 
def cached_can_be_reverted?
Loading
Loading
Loading
Loading
@@ -11,6 +11,9 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :merge_user_id
expose :merge_when_pipeline_succeeds
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_full_path do |merge_request|
merge_request.source_project&.full_path
Loading
Loading
@@ -240,6 +243,10 @@ class MergeRequestWidgetEntity < IssuableEntity
 
expose :supports_suggestion?, as: :can_receive_suggestion
 
expose :conflicts_docs_path do |merge_request|
presenter(merge_request).conflicts_docs_path
end
private
 
delegate :current_user, to: :request
Loading
Loading
Loading
Loading
@@ -7953,6 +7953,9 @@ msgstr[1] ""
msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
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"
msgstr ""
 
Loading
Loading
@@ -8112,6 +8115,9 @@ msgstr ""
msgid "mrWidget|There are unresolved discussions. Please resolve these discussions"
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"
msgstr ""
 
Loading
Loading
Loading
Loading
@@ -120,7 +120,9 @@
"rebase_path": { "type": ["string", "null"] },
"squash": { "type": "boolean" },
"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
}
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import $ from 'jquery';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import { removeBreakLine } from 'spec/helpers/vue_component_helper';
 
describe('MRWidgetConflicts', () => {
let Component;
let vm;
const path = '/conflicts';
 
function createComponent(propsData = {}) {
const localVue = createLocalVue();
vm = shallowMount(localVue.extend(ConflictsComponent), {
propsData,
});
}
beforeEach(() => {
Component = Vue.extend(conflictsComponent);
spyOn($.fn, 'popover').and.callThrough();
});
 
afterEach(() => {
vm.$destroy();
vm.destroy();
});
 
describe('when allowed to merge', () => {
beforeEach(() => {
vm = mountComponent(Component, {
createComponent({
mr: {
canMerge: true,
conflictResolutionPath: path,
conflictsDocsPath: '',
},
});
});
 
it('should tell you about conflicts without bothering other people', () => {
expect(vm.$el.textContent).toContain('There are merge conflicts');
expect(vm.$el.textContent).not.toContain('ask someone with write access');
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.$el.querySelector('.js-resolve-conflicts-button');
const resolveButton = vm.find('.js-resolve-conflicts-button');
 
expect(resolveButton.textContent).toContain('Resolve conflicts');
expect(resolveButton.getAttribute('href')).toEqual(path);
expect(resolveButton.text()).toContain('Resolve conflicts');
expect(resolveButton.attributes('href')).toEqual(path);
});
 
it('should have merge buttons', () => {
const mergeButton = vm.$el.querySelector('.js-disabled-merge-button');
const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
const mergeLocallyButton = vm.find('.js-merge-locally-button');
 
expect(mergeButton.textContent).toContain('Merge');
expect(mergeButton.disabled).toBeTruthy();
expect(mergeButton.classList.contains('btn-success')).toEqual(true);
expect(mergeLocallyButton.textContent).toContain('Merge locally');
expect(mergeLocallyButton.text()).toContain('Merge locally');
});
});
 
describe('when user does not have permission to merge', () => {
beforeEach(() => {
vm = mountComponent(Component, {
it('should show proper message', () => {
createComponent({
mr: {
canMerge: false,
conflictsDocsPath: '',
},
});
});
 
it('should show proper message', () => {
expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain(
'ask someone with write access',
);
expect(
vm
.text()
.trim()
.replace(/\s\s+/g, ' '),
).toContain('ask someone with write access');
});
 
it('should not have action buttons', () => {
expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined();
expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull();
expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull();
createComponent({
mr: {
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', () => {
beforeEach(() => {
vm = mountComponent(Component, {
it('should tell you to rebase locally', () => {
createComponent({
mr: {
shouldBeRebased: true,
conflictsDocsPath: '',
},
});
});
 
it('should tell you to rebase locally', () => {
expect(removeBreakLine(vm.$el.textContent).trim()).toContain(
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,
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();
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment