gitlab-org--gitlab-foss/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue

460 lines
16 KiB
Vue
Raw Normal View History

2018-03-05 22:24:16 +00:00
<script>
/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
import { GlIcon, GlButton, GlSprintf, GlLink } from '@gitlab/ui';
2017-05-09 04:15:34 +00:00
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
2017-05-09 04:15:34 +00:00
import simplePoll from '~/lib/utils/simple_poll';
import { __, sprintf } from '~/locale';
import MergeRequest from '../../../merge_request';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { deprecatedCreateFlash as Flash } from '../../../flash';
import statusIcon from '../mr_widget_status_icon.vue';
2017-05-09 04:15:34 +00:00
import eventHub from '../../event_hub';
import SquashBeforeMerge from './squash_before_merge.vue';
import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
import { AUTO_MERGE_STRATEGIES, DANGER, INFO, WARNING } from '../../constants';
const PIPELINE_RUNNING_STATE = 'running';
const PIPELINE_FAILED_STATE = 'failed';
const PIPELINE_PENDING_STATE = 'pending';
const PIPELINE_SUCCESS_STATE = 'success';
const MERGE_FAILED_STATUS = 'failed';
const MERGE_SUCCESS_STATUS = 'success';
const MERGE_HOOK_VALIDATION_ERROR_STATUS = 'hook_validation_error';
2017-05-09 04:15:34 +00:00
export default {
2018-03-05 22:24:16 +00:00
name: 'ReadyToMerge',
components: {
statusIcon,
SquashBeforeMerge,
CommitsHeader,
CommitEdit,
CommitMessageDropdown,
GlIcon,
GlSprintf,
GlLink,
GlButton,
MergeTrainHelperText: () =>
import('ee_component/vue_merge_request_widget/components/merge_train_helper_text.vue'),
MergeImmediatelyConfirmationDialog: () =>
import(
'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
),
2018-03-05 22:24:16 +00:00
},
mixins: [readyToMergeMixin],
2017-05-09 04:15:34 +00:00
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
removeSourceBranch: this.mr.shouldRemoveSourceBranch,
2017-05-09 04:15:34 +00:00
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
squashBeforeMerge: this.mr.squashIsSelected,
isSquashReadOnly: this.mr.squashIsReadonly,
2017-05-09 04:15:34 +00:00
successSvg,
warningSvg,
squashCommitMessage: this.mr.squashCommitMessage,
2017-05-09 04:15:34 +00:00
};
},
computed: {
isAutoMergeAvailable() {
return !isEmpty(this.mr.availableAutoMergeStrategies);
},
status() {
const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
2017-05-09 04:15:34 +00:00
if ((hasCI && !ciStatus) || this.hasPipelineMustSucceedConflict) {
return PIPELINE_FAILED_STATE;
}
if (this.isAutoMergeAvailable) {
return PIPELINE_PENDING_STATE;
}
if (pipeline && isPipelineFailed) {
return PIPELINE_FAILED_STATE;
}
return PIPELINE_SUCCESS_STATE;
},
mergeButtonVariant() {
if (this.status === PIPELINE_FAILED_STATE) {
return DANGER;
2017-05-09 04:15:34 +00:00
}
if (this.status === PIPELINE_PENDING_STATE) {
return INFO;
}
return PIPELINE_SUCCESS_STATE;
2017-05-09 04:15:34 +00:00
},
iconClass() {
if (this.shouldRenderMergeTrainHelperText && !this.mr.preventMerge) {
return PIPELINE_RUNNING_STATE;
}
if (
this.status === PIPELINE_FAILED_STATE ||
!this.commitMessage.length ||
!this.mr.isMergeAllowed ||
this.mr.preventMerge
) {
return WARNING;
}
return PIPELINE_SUCCESS_STATE;
},
2017-05-09 04:15:34 +00:00
mergeButtonText() {
if (this.isMergingImmediately) {
return __('Merge in progress');
}
if (this.isAutoMergeAvailable) {
return this.autoMergeText;
2017-05-09 04:15:34 +00:00
}
return __('Merge');
2017-05-09 04:15:34 +00:00
},
hasPipelineMustSucceedConflict() {
return !this.mr.hasCI && this.mr.onlyAllowMergeIfPipelineSucceeds;
},
isRemoveSourceBranchButtonDisabled() {
return this.isMergeButtonDisabled;
},
2017-05-09 04:15:34 +00:00
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge, squashIsReadonly, squashIsSelected } = this.mr;
if (squashIsReadonly && !squashIsSelected) {
return false;
}
2017-05-09 04:15:34 +00:00
return enableSquashBeforeMerge && commitsCount > 1;
},
shouldShowMergeControls() {
return this.mr.isMergeAllowed || this.isAutoMergeAvailable;
2017-05-09 04:15:34 +00:00
},
shouldShowSquashEdit() {
return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
},
shouldShowMergeEdit() {
return !this.mr.ffOnlyEnabled;
},
shaMismatchLink() {
const href = this.mr.mergeRequestDiffsPath;
return sprintf(
__('New changes were added. %{linkStart}Reload the page to review them%{linkEnd}'),
{
linkStart: `<a href="${href}">`,
linkEnd: '</a>',
},
false,
);
},
},
methods: {
updateMergeCommitMessage(includeDescription) {
const { commitMessageWithDescription, commitMessage } = this.mr;
this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
2017-05-09 04:15:34 +00:00
},
handleMergeButtonClick(useAutoMerge, mergeImmediately = false) {
if (mergeImmediately) {
2017-05-09 04:15:34 +00:00
this.isMergingImmediately = true;
}
const options = {
sha: this.mr.latestSHA || this.mr.sha,
2017-05-09 04:15:34 +00:00
commit_message: this.commitMessage,
auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined,
2017-05-09 04:15:34 +00:00
should_remove_source_branch: this.removeSourceBranch === true,
squash: this.squashBeforeMerge,
2017-05-09 04:15:34 +00:00
};
// If users can't alter the squash message (e.g. for 1-commit merge requests),
// we shouldn't send the commit message because that would make the backend
// do unnecessary work.
if (this.shouldShowSquashBeforeMerge) {
options.squash_commit_message = this.squashCommitMessage;
}
2017-05-09 04:15:34 +00:00
this.isMakingRequest = true;
this.service
.merge(options)
.then(res => res.data)
.then(data => {
const hasError =
data.status === MERGE_FAILED_STATUS ||
data.status === MERGE_HOOK_VALIDATION_ERROR_STATUS;
2017-05-09 04:15:34 +00:00
if (AUTO_MERGE_STRATEGIES.includes(data.status)) {
2017-05-09 04:15:34 +00:00
eventHub.$emit('MRWidgetUpdateRequested');
} else if (data.status === MERGE_SUCCESS_STATUS) {
2017-05-09 04:15:34 +00:00
this.initiateMergePolling();
} else if (hasError) {
eventHub.$emit('FailedToMerge', data.merge_error);
2017-05-09 04:15:34 +00:00
}
})
.catch(() => {
this.isMakingRequest = false;
new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
2017-05-09 04:15:34 +00:00
});
},
handleMergeImmediatelyButtonClick() {
if (this.isMergeImmediatelyDangerous) {
this.$refs.confirmationDialog.show();
} else {
this.handleMergeButtonClick(false, true);
}
},
onMergeImmediatelyConfirmation() {
this.handleMergeButtonClick(false, true);
},
2017-05-09 04:15:34 +00:00
initiateMergePolling() {
simplePoll(
(continuePolling, stopPolling) => {
this.handleMergePolling(continuePolling, stopPolling);
},
{ timeout: 0 },
);
2017-05-09 04:15:34 +00:00
},
handleMergePolling(continuePolling, stopPolling) {
this.service
.poll()
.then(res => res.data)
.then(data => {
if (data.state === 'merged') {
2017-05-09 04:15:34 +00:00
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('FetchActionsContent');
MergeRequest.setStatusBoxToMerged();
MergeRequest.hideCloseButton();
MergeRequest.decreaseCounter();
2017-05-09 04:15:34 +00:00
stopPolling();
refreshUserMergeRequestCounts();
2017-05-09 04:15:34 +00:00
// If user checked remove source branch and we didn't remove the branch yet
// we should start another polling for source branch remove process
if (this.removeSourceBranch && data.source_branch_exists) {
2017-05-09 04:15:34 +00:00
this.initiateRemoveSourceBranchPolling();
}
} else if (data.merge_error) {
eventHub.$emit('FailedToMerge', data.merge_error);
2017-05-09 04:15:34 +00:00
stopPolling();
} else {
// MR is not merged yet, continue polling until the state becomes 'merged'
continuePolling();
}
})
.catch(() => {
new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
stopPolling();
2017-05-09 04:15:34 +00:00
});
},
initiateRemoveSourceBranchPolling() {
// We need to show source branch is being removed spinner in another component
eventHub.$emit('SetBranchRemoveFlag', [true]);
simplePoll((continuePolling, stopPolling) => {
this.handleRemoveBranchPolling(continuePolling, stopPolling);
});
},
handleRemoveBranchPolling(continuePolling, stopPolling) {
this.service
.poll()
.then(res => res.data)
.then(data => {
2017-05-09 04:15:34 +00:00
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (data.source_branch_exists) {
2017-05-09 04:15:34 +00:00
continuePolling();
} else {
// Branch is removed. Update widget, stop polling and hide the spinner
eventHub.$emit('MRWidgetUpdateRequested', () => {
eventHub.$emit('SetBranchRemoveFlag', [false]);
});
stopPolling();
}
})
.catch(() => {
new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line
2017-05-09 04:15:34 +00:00
});
},
},
2018-03-05 22:24:16 +00:00
};
</script>
<template>
<div>
<div class="mr-widget-body media" :class="{ 'gl-pb-3': shouldRenderMergeTrainHelperText }">
<status-icon :status="iconClass" />
<div class="media-body">
<div class="mr-widget-body-controls media space-children">
<span class="btn-group">
<gl-button
size="medium"
category="primary"
class="qa-merge-button accept-merge-request"
:variant="mergeButtonVariant"
2017-08-07 02:29:37 +00:00
:disabled="isMergeButtonDisabled"
:loading="isMakingRequest"
@click="handleMergeButtonClick(isAutoMergeAvailable)"
>{{ mergeButtonText }}</gl-button
2018-11-16 20:07:38 +00:00
>
<button
v-if="shouldShowMergeImmediatelyDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
data-toggle="dropdown"
data-qa-selector="merge_moment_dropdown"
:aria-label="__('Select merge moment')"
>
<i class="fa fa-chevron-down" aria-hidden="true"></i>
</button>
<ul
v-if="shouldShowMergeImmediatelyDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu"
>
<li>
<a
class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
href="#"
@click.prevent="handleMergeButtonClick(true)"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
<span class="media-body merge-opt-title">{{ autoMergeText }}</span>
</span>
</a>
</li>
<li>
<merge-immediately-confirmation-dialog
ref="confirmationDialog"
:docs-url="mr.mergeImmediatelyDocsPath"
@mergeImmediately="onMergeImmediatelyConfirmation"
/>
<a
class="accept-merge-request js-merge-immediately-button"
data-qa-selector="merge_immediately_option"
href="#"
@click.prevent="handleMergeImmediatelyButtonClick"
>
<span class="media">
<span class="merge-opt-icon" aria-hidden="true" v-html="warningSvg"></span>
<span class="media-body merge-opt-title">{{ __('Merge immediately') }}</span>
</span>
</a>
</li>
</ul>
</span>
<div class="media-body-wrap space-children">
<template v-if="shouldShowMergeControls">
<label v-if="mr.canRemoveSourceBranch">
<input
id="remove-source-branch-input"
v-model="removeSourceBranch"
:disabled="isRemoveSourceBranchButtonDisabled"
class="js-remove-source-branch-checkbox"
type="checkbox"
/>
{{ __('Delete source branch') }}
</label>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
v-model="squashBeforeMerge"
:help-path="mr.squashBeforeMergeHelpPath"
:is-disabled="isSquashReadOnly"
/>
</template>
<template v-else>
<div class="bold js-resolve-mr-widget-items-message">
<div
v-if="hasPipelineMustSucceedConflict"
class="gl-display-flex gl-align-items-center"
data-testid="pipeline-succeed-conflict"
>
<gl-sprintf :message="pipelineMustSucceedConflictText" />
<gl-link
:href="mr.pipelineMustSucceedDocsPath"
target="_blank"
class="gl-display-flex gl-ml-2"
>
<gl-icon name="question" />
</gl-link>
</div>
<gl-sprintf v-else :message="mergeDisabledText" />
</div>
</template>
2017-05-09 04:15:34 +00:00
</div>
</div>
<div v-if="mr.isSHAMismatch" class="d-flex align-items-center mt-2 js-sha-mismatch">
<gl-icon name="warning-solid" class="text-warning mr-1" />
<span class="text-warning" v-html="shaMismatchLink"></span>
</div>
2017-08-07 02:29:37 +00:00
</div>
2017-05-09 04:15:34 +00:00
</div>
<merge-train-helper-text
v-if="shouldRenderMergeTrainHelperText"
:pipeline-id="mr.pipeline.id"
:pipeline-link="mr.pipeline.path"
:merge-train-length="mr.mergeTrainsCount"
:merge-train-when-pipeline-succeeds-docs-path="mr.mergeTrainWhenPipelineSucceedsDocsPath"
/>
<template v-if="shouldShowMergeControls">
<div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
{{ __('Fast-forward merge without a merge commit') }}
</div>
<commits-header
v-if="shouldShowSquashEdit || shouldShowMergeEdit"
:is-squash-enabled="squashBeforeMerge"
:commits-count="mr.commitsCount"
:target-branch="mr.targetBranch"
:is-fast-forward-enabled="mr.ffOnlyEnabled"
:class="{ 'border-bottom': mr.mergeError }"
>
<ul class="border-top content-list commits-list flex-list">
<commit-edit
v-if="shouldShowSquashEdit"
v-model="squashCommitMessage"
:label="__('Squash commit message')"
input-id="squash-message-edit"
squash
>
<commit-message-dropdown
slot="header"
v-model="squashCommitMessage"
:commits="mr.commits"
/>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
v-model="commitMessage"
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
<label slot="checkbox">
<input
id="include-description"
type="checkbox"
@change="updateMergeCommitMessage($event.target.checked)"
/>
{{ __('Include merge request description') }}
</label>
</commit-edit>
</ul>
</commits-header>
</template>
2018-03-05 22:24:16 +00:00
</div>
</template>