Commit 35ae9d8a authored by Nathan Friend's avatar Nathan Friend

Add merge train support to MR merge button (CE)

This commit updates the merge request widget's "Merge" button to
support merge trains.
parent fc27c93e
<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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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">
......
<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'; ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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 { ...@@ -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>
......
...@@ -3,3 +3,13 @@ export const DANGER = 'danger'; ...@@ -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,
];
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');
},
},
};
import { __ } from '~/locale';
export default { export default {
computed: { computed: {
isMergeButtonDisabled() { isMergeButtonDisabled() {
...@@ -9,5 +11,9 @@ export default { ...@@ -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');
},
}, },
}; };
...@@ -29,7 +29,7 @@ import UnresolvedDiscussionsState from './components/states/unresolved_discussio ...@@ -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 { ...@@ -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,
......
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 { ...@@ -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 { ...@@ -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 { ...@@ -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;
}
} }
...@@ -13,7 +13,7 @@ const stateToComponentMap = { ...@@ -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',
......
---
title: Update the merge request widget's "Merge" button to support merge trains
merge_request: 27594
author:
type: added
...@@ -6045,6 +6045,9 @@ msgstr "" ...@@ -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 ""
......
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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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();
});
});
}); });
}); });
...@@ -6,6 +6,7 @@ import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit ...@@ -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 => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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', () => { ...@@ -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);
}); });
......
...@@ -25,7 +25,6 @@ export default { ...@@ -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 { ...@@ -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 { ...@@ -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 = {
......
...@@ -82,5 +82,47 @@ describe('MergeRequestStore', () => { ...@@ -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();
});
});
}); });
}); });
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