Merge branch '9186-implement-atmtwps-state-to-mr-widget' into 'master'

CE backport for gitlab-org/gitlab-ee!12156: Update the merge request widget's "Merge" button to support merge trains

See merge request gitlab-org/gitlab-ce!27594
This commit is contained in:
Filipa Lacerda 2019-06-12 09:20:16 +00:00
commit 6cf619668b
14 changed files with 273 additions and 104 deletions

View file

@ -1,15 +1,19 @@
<script> <script>
import _ from 'underscore';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import Flash from '../../../flash'; import Flash from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue'; import statusIcon from '../mr_widget_status_icon.vue';
import MrWidgetAuthor from '../../components/mr_widget_author.vue'; import MrWidgetAuthor from '../../components/mr_widget_author.vue';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
export default { export default {
name: 'MRWidgetMergeWhenPipelineSucceeds', name: 'MRWidgetAutoMergeEnabled',
components: { components: {
MrWidgetAuthor, MrWidgetAuthor,
statusIcon, statusIcon,
}, },
mixins: [autoMergeMixin],
props: { props: {
mr: { mr: {
type: Object, type: Object,
@ -57,7 +61,7 @@ export default {
removeSourceBranch() { removeSourceBranch() {
const options = { const options = {
sha: this.mr.sha, sha: this.mr.sha,
auto_merge_strategy: 'merge_when_pipeline_succeeds', auto_merge_strategy: this.mr.autoMergeStrategy,
should_remove_source_branch: true, should_remove_source_branch: true,
}; };
@ -66,7 +70,7 @@ export default {
.merge(options) .merge(options)
.then(res => res.data) .then(res => res.data)
.then(data => { .then(data => {
if (data.status === 'merge_when_pipeline_succeeds') { if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) {
eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('MRWidgetUpdateRequested');
} }
}) })
@ -84,9 +88,9 @@ export default {
<div class="media-body"> <div class="media-body">
<h4 class="d-flex align-items-start"> <h4 class="d-flex align-items-start">
<span class="append-right-10"> <span class="append-right-10">
{{ s__('mrWidget|Set by') }} <span class="js-status-text-before-author">{{ statusTextBeforeAuthor }}</span>
<mr-widget-author :author="mr.setToAutoMergeBy" /> <mr-widget-author :author="mr.setToAutoMergeBy" />
{{ s__('mrWidget|to be merged automatically when the pipeline succeeds') }} <span class="js-status-text-after-author">{{ statusTextAfterAuthor }}</span>
</span> </span>
<a <a
v-if="mr.canCancelAutomaticMerge" v-if="mr.canCancelAutomaticMerge"
@ -97,7 +101,7 @@ export default {
@click.prevent="cancelAutomaticMerge" @click.prevent="cancelAutomaticMerge"
> >
<i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" aria-hidden="true"> </i> <i v-if="isCancellingAutoMerge" class="fa fa-spinner fa-spin" aria-hidden="true"> </i>
{{ s__('mrWidget|Cancel automatic merge') }} {{ cancelButtonText }}
</a> </a>
</h4> </h4>
<section class="mr-info-list"> <section class="mr-info-list">

View file

@ -1,4 +1,5 @@
<script> <script>
import _ from 'underscore';
import successSvg from 'icons/_icon_status_success.svg'; import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg'; import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll'; import simplePoll from '~/lib/utils/simple_poll';
@ -12,6 +13,7 @@ import SquashBeforeMerge from './squash_before_merge.vue';
import CommitsHeader from './commits_header.vue'; import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue'; import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue'; import CommitMessageDropdown from './commit_message_dropdown.vue';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
export default { export default {
name: 'ReadyToMerge', name: 'ReadyToMerge',
@ -30,8 +32,6 @@ export default {
data() { data() {
return { return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch, removeSourceBranch: this.mr.shouldRemoveSourceBranch,
mergeWhenBuildSucceeds: false,
autoMergeStrategy: undefined,
isMakingRequest: false, isMakingRequest: false,
isMergingImmediately: false, isMergingImmediately: false,
commitMessage: this.mr.commitMessage, commitMessage: this.mr.commitMessage,
@ -42,18 +42,18 @@ export default {
}; };
}, },
computed: { computed: {
shouldShowAutoMergeText() { isAutoMergeAvailable() {
return this.mr.isPipelineActive; return !_.isEmpty(this.mr.availableAutoMergeStrategies);
}, },
status() { status() {
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr; const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) { if (hasCI && !ciStatus) {
return 'failed'; return 'failed';
} else if (this.isAutoMergeAvailable) {
return 'pending';
} else if (!pipeline) { } else if (!pipeline) {
return 'success'; return 'success';
} else if (isPipelineActive) {
return 'pending';
} else if (isPipelineFailed) { } else if (isPipelineFailed) {
return 'failed'; return 'failed';
} }
@ -87,14 +87,14 @@ export default {
mergeButtonText() { mergeButtonText() {
if (this.isMergingImmediately) { if (this.isMergingImmediately) {
return __('Merge in progress'); return __('Merge in progress');
} else if (this.shouldShowAutoMergeText) { } else if (this.isAutoMergeAvailable) {
return __('Merge when pipeline succeeds'); return this.autoMergeText;
} }
return 'Merge'; return __('Merge');
}, },
shouldShowMergeOptionsDropdown() { shouldShowMergeOptionsDropdown() {
return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds; return this.isAutoMergeAvailable && !this.mr.onlyAllowMergeIfPipelineSucceeds;
}, },
isRemoveSourceBranchButtonDisabled() { isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled; return this.isMergeButtonDisabled;
@ -104,7 +104,7 @@ export default {
return enableSquashBeforeMerge && commitsCount > 1; return enableSquashBeforeMerge && commitsCount > 1;
}, },
shouldShowMergeControls() { shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.shouldShowAutoMergeText; return this.mr.isMergeAllowed || this.isAutoMergeAvailable;
}, },
shouldShowSquashEdit() { shouldShowSquashEdit() {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge; return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
@ -118,20 +118,15 @@ export default {
const { commitMessageWithDescription, commitMessage } = this.mr; const { commitMessageWithDescription, commitMessage } = this.mr;
this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage; this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
}, },
handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) { handleMergeButtonClick(useAutoMerge, mergeImmediately = false) {
// TODO: Remove no-param-reassign if (mergeImmediately) {
if (mergeWhenBuildSucceeds === undefined) {
mergeWhenBuildSucceeds = this.mr.isPipelineActive; // eslint-disable-line no-param-reassign
} else if (mergeImmediately) {
this.isMergingImmediately = true; this.isMergingImmediately = true;
} }
this.autoMergeStrategy = mergeWhenBuildSucceeds ? 'merge_when_pipeline_succeeds' : undefined;
const options = { const options = {
sha: this.mr.sha, sha: this.mr.sha,
commit_message: this.commitMessage, commit_message: this.commitMessage,
auto_merge_strategy: this.autoMergeStrategy, auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined,
should_remove_source_branch: this.removeSourceBranch === true, should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge, squash: this.squashBeforeMerge,
squash_commit_message: this.squashCommitMessage, squash_commit_message: this.squashCommitMessage,
@ -144,7 +139,7 @@ export default {
.then(data => { .then(data => {
const hasError = data.status === 'failed' || data.status === 'hook_validation_error'; const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
if (data.status === 'merge_when_pipeline_succeeds') { if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) {
eventHub.$emit('MRWidgetUpdateRequested'); eventHub.$emit('MRWidgetUpdateRequested');
} else if (data.status === 'success') { } else if (data.status === 'success') {
this.initiateMergePolling(); this.initiateMergePolling();
@ -242,13 +237,13 @@ export default {
:class="mergeButtonClass" :class="mergeButtonClass"
type="button" type="button"
class="qa-merge-button" class="qa-merge-button"
@click="handleMergeButtonClick()" @click="handleMergeButtonClick(isAutoMergeAvailable)"
> >
<i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i> <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
{{ mergeButtonText }} {{ mergeButtonText }}
</button> </button>
<button <button
v-if="shouldShowMergeOptionsDropdown" v-if="isAutoMergeAvailable"
:disabled="isMergeButtonDisabled" :disabled="isMergeButtonDisabled"
type="button" type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment" class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
@ -264,15 +259,13 @@ export default {
> >
<li> <li>
<a <a
class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
href="#" href="#"
@click.prevent="handleMergeButtonClick(true)" @click.prevent="handleMergeButtonClick(true)"
> >
<span class="media"> <span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span> <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
<span class="media-body merge-opt-title">{{ <span class="media-body merge-opt-title">{{ autoMergeText }}</span>
__('Merge when pipeline succeeds')
}}</span>
</span> </span>
</a> </a>
</li> </li>

View file

@ -3,3 +3,13 @@ export const DANGER = 'danger';
export const WARNING_MESSAGE_CLASS = 'warning_message'; export const WARNING_MESSAGE_CLASS = 'warning_message';
export const DANGER_MESSAGE_CLASS = 'danger_message'; export const DANGER_MESSAGE_CLASS = 'danger_message';
export const MWPS_MERGE_STRATEGY = 'merge_when_pipeline_succeeds';
export const ATMTWPS_MERGE_STRATEGY = 'add_to_merge_train_when_pipeline_succeeds';
export const MT_MERGE_STRATEGY = 'merge_train';
export const AUTO_MERGE_STRATEGIES = [
MWPS_MERGE_STRATEGY,
ATMTWPS_MERGE_STRATEGY,
MT_MERGE_STRATEGY,
];

View file

@ -0,0 +1,15 @@
import { s__ } from '~/locale';
export default {
computed: {
statusTextBeforeAuthor() {
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
},
cancelButtonText() {
return s__('mrWidget|Cancel automatic merge');
},
},
};

View file

@ -1,3 +1,5 @@
import { __ } from '~/locale';
export default { export default {
computed: { computed: {
isMergeButtonDisabled() { isMergeButtonDisabled() {
@ -9,5 +11,9 @@ export default {
this.mr.preventMerge, this.mr.preventMerge,
); );
}, },
autoMergeText() {
// MWPS is currently the only auto merge strategy available in CE
return __('Merge when pipeline succeeds');
},
}, },
}; };

View file

@ -29,7 +29,7 @@ import UnresolvedDiscussionsState from './components/states/unresolved_discussio
import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue'; import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked.vue';
import PipelineFailedState from './components/states/pipeline_failed.vue'; import PipelineFailedState from './components/states/pipeline_failed.vue';
import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue';
import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds.vue'; import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue';
import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue';
import CheckingState from './components/states/mr_widget_checking.vue'; import CheckingState from './components/states/mr_widget_checking.vue';
import eventHub from './event_hub'; import eventHub from './event_hub';
@ -64,7 +64,7 @@ export default {
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
'mr-widget-pipeline-blocked': PipelineBlockedState, 'mr-widget-pipeline-blocked': PipelineBlockedState,
'mr-widget-pipeline-failed': PipelineFailedState, 'mr-widget-pipeline-failed': PipelineFailedState,
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, MrWidgetAutoMergeEnabled,
'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-auto-merge-failed': AutoMergeFailed,
'mr-widget-rebase': RebaseState, 'mr-widget-rebase': RebaseState,
SourceBranchRemovalStatus, SourceBranchRemovalStatus,

View file

@ -1,7 +1,9 @@
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import _ from 'underscore';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key'; import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { stateKey } from './state_maps'; import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility'; import { formatDate } from '../../lib/utils/datetime_utility';
import { ATMTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
export default class MergeRequestStore { export default class MergeRequestStore {
constructor(data) { constructor(data) {
@ -77,6 +79,10 @@ export default class MergeRequestStore {
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false; this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.autoMergeEnabled = Boolean(data.auto_merge_enabled); this.autoMergeEnabled = Boolean(data.auto_merge_enabled);
this.autoMergeStrategy = data.auto_merge_strategy; this.autoMergeStrategy = data.auto_merge_strategy;
this.availableAutoMergeStrategies = data.available_auto_merge_strategies;
this.preferredAutoMergeStrategy = MergeRequestStore.getPreferredAutoMergeStrategy(
this.availableAutoMergeStrategies,
);
this.mergePath = data.merge_path; this.mergePath = data.merge_path;
this.ffOnlyEnabled = data.ff_only_enabled; this.ffOnlyEnabled = data.ff_only_enabled;
this.shouldBeRebased = Boolean(data.should_be_rebased); this.shouldBeRebased = Boolean(data.should_be_rebased);
@ -104,7 +110,9 @@ export default class MergeRequestStore {
this.sourceProjectFullPath = data.source_project_full_path; this.sourceProjectFullPath = data.source_project_full_path;
this.sourceProjectId = data.source_project_id; this.sourceProjectId = data.source_project_id;
this.targetProjectId = data.target_project_id; this.targetProjectId = data.target_project_id;
this.mergePipelinesEnabled = data.merge_pipelines_enabled; this.mergePipelinesEnabled = Boolean(data.merge_pipelines_enabled);
this.mergeTrainsCount = data.merge_trains_count || 0;
this.mergeTrainIndex = data.merge_train_index;
// Cherry-pick and Revert actions related // Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false; this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
@ -204,4 +212,16 @@ export default class MergeRequestStore {
return timeagoInstance.format(date); return timeagoInstance.format(date);
} }
static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {
if (_.includes(availableAutoMergeStrategies, ATMTWPS_MERGE_STRATEGY)) {
return ATMTWPS_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MT_MERGE_STRATEGY)) {
return MT_MERGE_STRATEGY;
} else if (_.includes(availableAutoMergeStrategies, MWPS_MERGE_STRATEGY)) {
return MWPS_MERGE_STRATEGY;
}
return undefined;
}
} }

View file

@ -13,7 +13,7 @@ const stateToComponentMap = {
unresolvedDiscussions: 'mr-widget-unresolved-discussions', unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked', pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed', pipelineFailed: 'mr-widget-pipeline-failed',
autoMergeEnabled: 'mr-widget-merge-when-pipeline-succeeds', autoMergeEnabled: 'mr-widget-auto-merge-enabled',
failedToMerge: 'mr-widget-failed-to-merge', failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed', autoMergeFailed: 'mr-widget-auto-merge-failed',
shaMismatch: 'sha-mismatch', shaMismatch: 'sha-mismatch',

View file

@ -0,0 +1,5 @@
---
title: Update the merge request widget's "Merge" button to support merge trains
merge_request: 27594
author:
type: added

View file

@ -6045,6 +6045,9 @@ msgstr ""
msgid "Members of <strong>%{project_name}</strong>" msgid "Members of <strong>%{project_name}</strong>"
msgstr "" msgstr ""
msgid "Merge"
msgstr ""
msgid "Merge Request" msgid "Merge Request"
msgstr "" msgstr ""

View file

@ -1,17 +1,19 @@
import Vue from 'vue'; import Vue from 'vue';
import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue'; import autoMergeEnabledComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { trimText } from 'spec/helpers/text_helper';
import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
describe('MRWidgetMergeWhenPipelineSucceeds', () => { describe('MRWidgetAutoMergeEnabled', () => {
let vm; let vm;
const targetBranchPath = '/foo/bar'; const targetBranchPath = '/foo/bar';
const targetBranch = 'foo'; const targetBranch = 'foo';
const sha = '1EA2EZ34'; const sha = '1EA2EZ34';
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(mwpsComponent); const Component = Vue.extend(autoMergeEnabledComponent);
spyOn(eventHub, '$emit'); spyOn(eventHub, '$emit');
vm = mountComponent(Component, { vm = mountComponent(Component, {
@ -25,6 +27,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
sha, sha,
targetBranchPath, targetBranchPath,
targetBranch, targetBranch,
autoMergeStrategy: MWPS_MERGE_STRATEGY,
}, },
service: new MRWidgetService({}), service: new MRWidgetService({}),
}); });
@ -66,6 +69,32 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
expect(vm.canRemoveSourceBranch).toBeFalsy(); expect(vm.canRemoveSourceBranch).toBeFalsy();
}); });
}); });
describe('statusTextBeforeAuthor', () => {
it('should return "Set by" if the MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.statusTextBeforeAuthor).toBe('Set by');
});
});
describe('statusTextAfterAuthor', () => {
it('should return "to be merged automatically..." if MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.statusTextAfterAuthor).toBe(
'to be merged automatically when the pipeline succeeds',
);
});
});
describe('cancelButtonText', () => {
it('should return "Cancel automatic merge" if MWPS is selected', () => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.cancelButtonText).toBe('Cancel automatic merge');
});
});
}); });
describe('methods', () => { describe('methods', () => {
@ -96,7 +125,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
spyOn(vm.service, 'merge').and.returnValue( spyOn(vm.service, 'merge').and.returnValue(
Promise.resolve({ Promise.resolve({
data: { data: {
status: 'merge_when_pipeline_succeeds', status: MWPS_MERGE_STRATEGY,
}, },
}), }),
); );
@ -106,7 +135,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.merge).toHaveBeenCalledWith({ expect(vm.service.merge).toHaveBeenCalledWith({
sha, sha,
auto_merge_strategy: 'merge_when_pipeline_succeeds', auto_merge_strategy: MWPS_MERGE_STRATEGY,
should_remove_source_branch: true, should_remove_source_branch: true,
}); });
done(); done();
@ -119,6 +148,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
it('should have correct elements', () => { it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds'); expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds');
expect(vm.$el.innerText).toContain('The changes will be merged into'); expect(vm.$el.innerText).toContain('The changes will be merged into');
expect(vm.$el.innerText).toContain(targetBranch); expect(vm.$el.innerText).toContain(targetBranch);
expect(vm.$el.innerText).toContain('The source branch will not be deleted'); expect(vm.$el.innerText).toContain('The source branch will not be deleted');
@ -174,5 +204,27 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
done(); done();
}); });
}); });
it('should render the status text as "...to merged automatically" if MWPS is selected', done => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
Vue.nextTick(() => {
const statusText = trimText(vm.$el.querySelector('.js-status-text-after-author').innerText);
expect(statusText).toBe('to be merged automatically when the pipeline succeeds');
done();
});
});
it('should render the cancel button as "Cancel automatic merge" if MWPS is selected', done => {
Vue.set(vm.mr, 'autoMergeStrategy', MWPS_MERGE_STRATEGY);
Vue.nextTick(() => {
const cancelButtonText = trimText(vm.$el.querySelector('.js-cancel-auto-merge').innerText);
expect(cancelButtonText).toBe('Cancel automatic merge');
done();
});
});
}); });
}); });

View file

@ -6,6 +6,7 @@ import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { MWPS_MERGE_STRATEGY, ATMTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
const commitMessage = 'This is the commit message'; const commitMessage = 'This is the commit message';
const squashCommitMessage = 'This is the squash commit message'; const squashCommitMessage = 'This is the squash commit message';
@ -29,6 +30,8 @@ const createTestMr = customConfig => {
shouldRemoveSourceBranch: true, shouldRemoveSourceBranch: true,
canRemoveSourceBranch: false, canRemoveSourceBranch: false,
targetBranch: 'master', targetBranch: 'master',
preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY,
availableAutoMergeStrategies: [MWPS_MERGE_STRATEGY],
}; };
Object.assign(mr, customConfig.mr); Object.assign(mr, customConfig.mr);
@ -80,7 +83,6 @@ describe('ReadyToMerge', () => {
it('should have default data', () => { it('should have default data', () => {
expect(vm.mergeWhenBuildSucceeds).toBeFalsy(); expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy(); expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.showCommitMessageEditor).toBeFalsy(); expect(vm.showCommitMessageEditor).toBeFalsy();
expect(vm.isMakingRequest).toBeFalsy(); expect(vm.isMakingRequest).toBeFalsy();
expect(vm.isMergingImmediately).toBeFalsy(); expect(vm.isMergingImmediately).toBeFalsy();
@ -91,47 +93,51 @@ describe('ReadyToMerge', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('shouldShowAutoMergeText', () => { describe('isAutoMergeAvailable', () => {
it('should return true with active pipeline', () => { it('should return true when at least one merge strategy is available', () => {
vm.mr.isPipelineActive = true; vm.mr.availableAutoMergeStrategies = [MWPS_MERGE_STRATEGY];
expect(vm.shouldShowAutoMergeText).toBeTruthy(); expect(vm.isAutoMergeAvailable).toBe(true);
}); });
it('should return false with inactive pipeline', () => { it('should return false when no merge strategies are available', () => {
vm.mr.isPipelineActive = false; vm.mr.availableAutoMergeStrategies = [];
expect(vm.shouldShowAutoMergeText).toBeFalsy(); expect(vm.isAutoMergeAvailable).toBe(false);
}); });
}); });
describe('status', () => { describe('status', () => {
it('defaults to success', () => { it('defaults to success', () => {
vm.mr.pipeline = true; Vue.set(vm.mr, 'pipeline', true);
Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.status).toEqual('success'); expect(vm.status).toEqual('success');
}); });
it('returns failed when MR has CI but also has an unknown status', () => { it('returns failed when MR has CI but also has an unknown status', () => {
vm.mr.hasCI = true; Vue.set(vm.mr, 'hasCI', true);
expect(vm.status).toEqual('failed'); expect(vm.status).toEqual('failed');
}); });
it('returns default when MR has no pipeline', () => { it('returns default when MR has no pipeline', () => {
Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.status).toEqual('success'); expect(vm.status).toEqual('success');
}); });
it('returns pending when pipeline is active', () => { it('returns pending when pipeline is active', () => {
vm.mr.pipeline = {}; Vue.set(vm.mr, 'pipeline', {});
vm.mr.isPipelineActive = true; Vue.set(vm.mr, 'isPipelineActive', true);
expect(vm.status).toEqual('pending'); expect(vm.status).toEqual('pending');
}); });
it('returns failed when pipeline is failed', () => { it('returns failed when pipeline is failed', () => {
vm.mr.pipeline = {}; Vue.set(vm.mr, 'pipeline', {});
vm.mr.isPipelineFailed = true; Vue.set(vm.mr, 'isPipelineFailed', true);
Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.status).toEqual('failed'); expect(vm.status).toEqual('failed');
}); });
@ -143,18 +149,20 @@ describe('ReadyToMerge', () => {
const inActionClass = `${defaultClass} btn-info`; const inActionClass = `${defaultClass} btn-info`;
it('defaults to success class', () => { it('defaults to success class', () => {
Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.mergeButtonClass).toEqual(defaultClass); expect(vm.mergeButtonClass).toEqual(defaultClass);
}); });
it('returns success class for success status', () => { it('returns success class for success status', () => {
vm.mr.pipeline = true; Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
Vue.set(vm.mr, 'pipeline', true);
expect(vm.mergeButtonClass).toEqual(defaultClass); expect(vm.mergeButtonClass).toEqual(defaultClass);
}); });
it('returns info class for pending status', () => { it('returns info class for pending status', () => {
vm.mr.pipeline = {}; Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]);
vm.mr.isPipelineActive = true;
expect(vm.mergeButtonClass).toEqual(inActionClass); expect(vm.mergeButtonClass).toEqual(inActionClass);
}); });
@ -198,69 +206,82 @@ describe('ReadyToMerge', () => {
}); });
describe('mergeButtonText', () => { describe('mergeButtonText', () => {
it('should return Merge', () => { it('should return "Merge" when no auto merge strategies are available', () => {
Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.mergeButtonText).toEqual('Merge'); expect(vm.mergeButtonText).toEqual('Merge');
}); });
it('should return Merge in progress', () => { it('should return "Merge in progress"', () => {
vm.isMergingImmediately = true; Vue.set(vm, 'isMergingImmediately', true);
expect(vm.mergeButtonText).toEqual('Merge in progress'); expect(vm.mergeButtonText).toEqual('Merge in progress');
}); });
it('should return Merge when pipeline succeeds', () => { it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
vm.isMergingImmediately = false; Vue.set(vm, 'isMergingImmediately', false);
vm.mr.isPipelineActive = true; Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds'); expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds');
}); });
}); });
describe('autoMergeText', () => {
it('should return Merge when pipeline succeeds', () => {
Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY);
expect(vm.autoMergeText).toEqual('Merge when pipeline succeeds');
});
});
describe('shouldShowMergeOptionsDropdown', () => { describe('shouldShowMergeOptionsDropdown', () => {
it('should return false with initial data', () => { it('should return false when no auto merge strategies are available', () => {
expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy(); Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.shouldShowMergeOptionsDropdown).toBe(false);
}); });
it('should return true when pipeline active', () => { it('should return true when at least one auto merge strategy is available', () => {
vm.mr.isPipelineActive = true; Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]);
expect(vm.shouldShowMergeOptionsDropdown).toBeTruthy(); expect(vm.shouldShowMergeOptionsDropdown).toBe(true);
}); });
it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => { it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => {
vm.mr.isPipelineActive = true; Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]);
vm.mr.onlyAllowMergeIfPipelineSucceeds = true; Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true);
expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy(); expect(vm.shouldShowMergeOptionsDropdown).toBe(false);
}); });
}); });
describe('isMergeButtonDisabled', () => { describe('isMergeButtonDisabled', () => {
it('should return false with initial data', () => { it('should return false with initial data', () => {
vm.mr.isMergeAllowed = true; Vue.set(vm.mr, 'isMergeAllowed', true);
expect(vm.isMergeButtonDisabled).toBeFalsy(); expect(vm.isMergeButtonDisabled).toBe(false);
}); });
it('should return true when there is no commit message', () => { it('should return true when there is no commit message', () => {
vm.mr.isMergeAllowed = true; Vue.set(vm.mr, 'isMergeAllowed', true);
vm.commitMessage = ''; Vue.set(vm, 'commitMessage', '');
expect(vm.isMergeButtonDisabled).toBeTruthy(); expect(vm.isMergeButtonDisabled).toBe(true);
}); });
it('should return true if merge is not allowed', () => { it('should return true if merge is not allowed', () => {
vm.mr.isMergeAllowed = false; Vue.set(vm.mr, 'isMergeAllowed', false);
vm.mr.onlyAllowMergeIfPipelineSucceeds = true; Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true);
expect(vm.isMergeButtonDisabled).toBeTruthy(); expect(vm.isMergeButtonDisabled).toBe(true);
}); });
it('should return true when the vm instance is making request', () => { it('should return true when the vm instance is making request', () => {
vm.mr.isMergeAllowed = true; Vue.set(vm.mr, 'isMergeAllowed', true);
vm.isMakingRequest = true; Vue.set(vm, 'isMakingRequest', true);
expect(vm.isMergeButtonDisabled).toBeTruthy(); expect(vm.isMergeButtonDisabled).toBe(true);
}); });
}); });
}); });
@ -268,31 +289,31 @@ describe('ReadyToMerge', () => {
describe('methods', () => { describe('methods', () => {
describe('shouldShowMergeControls', () => { describe('shouldShowMergeControls', () => {
it('should return false when an external pipeline is running and required to succeed', () => { it('should return false when an external pipeline is running and required to succeed', () => {
vm.mr.isMergeAllowed = false; Vue.set(vm.mr, 'isMergeAllowed', false);
vm.mr.isPipelineActive = false; Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.shouldShowMergeControls).toBeFalsy(); expect(vm.shouldShowMergeControls).toBe(false);
}); });
it('should return true when the build succeeded or build not required to succeed', () => { it('should return true when the build succeeded or build not required to succeed', () => {
vm.mr.isMergeAllowed = true; Vue.set(vm.mr, 'isMergeAllowed', true);
vm.mr.isPipelineActive = false; Vue.set(vm.mr, 'availableAutoMergeStrategies', []);
expect(vm.shouldShowMergeControls).toBeTruthy(); expect(vm.shouldShowMergeControls).toBe(true);
}); });
it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => { it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => {
vm.mr.isMergeAllowed = false; Vue.set(vm.mr, 'isMergeAllowed', false);
vm.mr.isPipelineActive = true; Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]);
expect(vm.shouldShowMergeControls).toBeTruthy(); expect(vm.shouldShowMergeControls).toBe(true);
}); });
it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => { it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => {
vm.mr.isMergeAllowed = true; Vue.set(vm.mr, 'isMergeAllowed', true);
vm.mr.isPipelineActive = true; Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]);
expect(vm.shouldShowMergeControls).toBeTruthy(); expect(vm.shouldShowMergeControls).toBe(true);
}); });
}); });
@ -325,7 +346,6 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(true); vm.handleMergeButtonClick(true);
setTimeout(() => { setTimeout(() => {
expect(vm.autoMergeStrategy).toBe('merge_when_pipeline_succeeds');
expect(vm.isMakingRequest).toBeTruthy(); expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
@ -349,14 +369,13 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(false, true); vm.handleMergeButtonClick(false, true);
setTimeout(() => { setTimeout(() => {
expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy(); expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined); expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
const params = vm.service.merge.calls.argsFor(0)[0]; const params = vm.service.merge.calls.argsFor(0)[0];
expect(params.should_remove_source_branch).toBeTruthy(); expect(params.should_remove_source_branch).toBeTruthy();
expect(params.merge_when_pipeline_succeeds).toBeFalsy(); expect(params.auto_merge_strategy).toBeUndefined();
done(); done();
}, 333); }, 333);
}); });
@ -367,14 +386,13 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(); vm.handleMergeButtonClick();
setTimeout(() => { setTimeout(() => {
expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy(); expect(vm.isMakingRequest).toBeTruthy();
expect(vm.initiateMergePolling).toHaveBeenCalled(); expect(vm.initiateMergePolling).toHaveBeenCalled();
const params = vm.service.merge.calls.argsFor(0)[0]; const params = vm.service.merge.calls.argsFor(0)[0];
expect(params.should_remove_source_branch).toBeTruthy(); expect(params.should_remove_source_branch).toBeTruthy();
expect(params.merge_when_pipeline_succeeds).toBeFalsy(); expect(params.auto_merge_strategy).toBeUndefined();
done(); done();
}, 333); }, 333);
}); });

View file

@ -25,7 +25,6 @@ export default {
}, },
merge_status: 'can_be_merged', merge_status: 'can_be_merged',
merge_user_id: null, merge_user_id: null,
merge_when_pipeline_succeeds: false,
source_branch: 'daaaa', source_branch: 'daaaa',
source_branch_link: 'daaaa', source_branch_link: 'daaaa',
source_project_id: 19, source_project_id: 19,
@ -210,8 +209,7 @@ export default {
source_branch_path: '/root/acets-app/branches/daaaa', source_branch_path: '/root/acets-app/branches/daaaa',
conflict_resolution_ui_path: '/root/acets-app/merge_requests/22/conflicts', conflict_resolution_ui_path: '/root/acets-app/merge_requests/22/conflicts',
remove_wip_path: '/root/acets-app/merge_requests/22/remove_wip', remove_wip_path: '/root/acets-app/merge_requests/22/remove_wip',
cancel_merge_when_pipeline_succeeds_path: cancel_auto_merge_path: '/root/acets-app/merge_requests/22/cancel_auto_merge',
'/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds',
create_issue_to_resolve_discussions_path: create_issue_to_resolve_discussions_path:
'/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22', '/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22',
merge_path: '/root/acets-app/merge_requests/22/merge', merge_path: '/root/acets-app/merge_requests/22/merge',
@ -237,6 +235,9 @@ export default {
merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md', merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
squash: true, squash: true,
visual_review_app_available: true, visual_review_app_available: true,
merge_trains_enabled: true,
merge_trains_count: 3,
merge_train_index: 1,
}; };
export const mockStore = { export const mockStore = {

View file

@ -82,5 +82,47 @@ describe('MergeRequestStore', () => {
expect(store.isNothingToMergeState).toEqual(false); expect(store.isNothingToMergeState).toEqual(false);
}); });
}); });
describe('mergePipelinesEnabled', () => {
it('should set mergePipelinesEnabled = true when merge_pipelines_enabled is true', () => {
store.setData({ ...mockData, merge_pipelines_enabled: true });
expect(store.mergePipelinesEnabled).toBe(true);
});
it('should set mergePipelinesEnabled = false when merge_pipelines_enabled is not provided', () => {
store.setData({ ...mockData, merge_pipelines_enabled: undefined });
expect(store.mergePipelinesEnabled).toBe(false);
});
});
describe('mergeTrainsCount', () => {
it('should set mergeTrainsCount when merge_trains_count is provided', () => {
store.setData({ ...mockData, merge_trains_count: 3 });
expect(store.mergeTrainsCount).toBe(3);
});
it('should set mergeTrainsCount = 0 when merge_trains_count is not provided', () => {
store.setData({ ...mockData, merge_trains_count: undefined });
expect(store.mergeTrainsCount).toBe(0);
});
});
describe('mergeTrainIndex', () => {
it('should set mergeTrainIndex when merge_train_index is provided', () => {
store.setData({ ...mockData, merge_train_index: 3 });
expect(store.mergeTrainIndex).toBe(3);
});
it('should not set mergeTrainIndex when merge_train_index is not provided', () => {
store.setData({ ...mockData, merge_train_index: undefined });
expect(store.mergeTrainIndex).toBeUndefined();
});
});
}); });
}); });