Read about our upcoming Code of Conduct on this issue

ready_to_merge.vue 14.1 KB
Newer Older
1
<script>
2
import _ from 'underscore';
3
import { GlIcon } from '@gitlab/ui';
Fatih Acet's avatar
Fatih Acet committed
4
5
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
6
import readyToMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/ready_to_merge';
Fatih Acet's avatar
Fatih Acet committed
7
import simplePoll from '~/lib/utils/simple_poll';
8
import { __, sprintf } from '~/locale';
9
import MergeRequest from '../../../merge_request';
10
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
Phil Hughes's avatar
Phil Hughes committed
11
import Flash from '../../../flash';
12
import statusIcon from '../mr_widget_status_icon.vue';
Fatih Acet's avatar
Fatih Acet committed
13
import eventHub from '../../event_hub';
14
import SquashBeforeMerge from './squash_before_merge.vue';
15
16
17
import CommitsHeader from './commits_header.vue';
import CommitEdit from './commit_edit.vue';
import CommitMessageDropdown from './commit_message_dropdown.vue';
18
import { AUTO_MERGE_STRATEGIES } from '../../constants';
Fatih Acet's avatar
Fatih Acet committed
19
20

export default {
21
22
23
  name: 'ReadyToMerge',
  components: {
    statusIcon,
24
    SquashBeforeMerge,
25
26
27
    CommitsHeader,
    CommitEdit,
    CommitMessageDropdown,
28
    GlIcon,
29
30
31
32
    MergeImmediatelyConfirmationDialog: () =>
      import(
        'ee_component/vue_merge_request_widget/components/merge_immediately_confirmation_dialog.vue'
      ),
33
  },
34
  mixins: [readyToMergeMixin],
Fatih Acet's avatar
Fatih Acet committed
35
36
37
38
39
40
  props: {
    mr: { type: Object, required: true },
    service: { type: Object, required: true },
  },
  data() {
    return {
41
      removeSourceBranch: this.mr.shouldRemoveSourceBranch,
Fatih Acet's avatar
Fatih Acet committed
42
43
44
      isMakingRequest: false,
      isMergingImmediately: false,
      commitMessage: this.mr.commitMessage,
45
      squashBeforeMerge: this.mr.squash,
Fatih Acet's avatar
Fatih Acet committed
46
47
      successSvg,
      warningSvg,
48
      squashCommitMessage: this.mr.squashCommitMessage,
Fatih Acet's avatar
Fatih Acet committed
49
50
51
    };
  },
  computed: {
52
53
    isAutoMergeAvailable() {
      return !_.isEmpty(this.mr.availableAutoMergeStrategies);
54
    },
55
    status() {
56
      const { pipeline, isPipelineFailed, hasCI, ciStatus } = this.mr;
Fatih Acet's avatar
Fatih Acet committed
57
58

      if (hasCI && !ciStatus) {
59
        return 'failed';
60
61
      } else if (this.isAutoMergeAvailable) {
        return 'pending';
Fatih Acet's avatar
Fatih Acet committed
62
      } else if (!pipeline) {
63
        return 'success';
Fatih Acet's avatar
Fatih Acet committed
64
      } else if (isPipelineFailed) {
65
66
67
68
69
70
71
72
73
74
75
        return 'failed';
      }

      return 'success';
    },
    mergeButtonClass() {
      const defaultClass = 'btn btn-sm btn-success accept-merge-request';
      const failedClass = `${defaultClass} btn-danger`;
      const inActionClass = `${defaultClass} btn-info`;

      if (this.status === 'failed') {
Fatih Acet's avatar
Fatih Acet committed
76
        return failedClass;
77
78
      } else if (this.status === 'pending') {
        return inActionClass;
Fatih Acet's avatar
Fatih Acet committed
79
80
81
82
      }

      return defaultClass;
    },
83
    iconClass() {
84
85
86
87
88
89
      if (
        this.status === 'failed' ||
        !this.commitMessage.length ||
        !this.mr.isMergeAllowed ||
        this.mr.preventMerge
      ) {
Filipa Lacerda's avatar
Filipa Lacerda committed
90
        return 'warning';
91
92
93
      }
      return 'success';
    },
Fatih Acet's avatar
Fatih Acet committed
94
95
    mergeButtonText() {
      if (this.isMergingImmediately) {
96
        return __('Merge in progress');
97
98
      }
      if (this.isAutoMergeAvailable) {
99
        return this.autoMergeText;
Fatih Acet's avatar
Fatih Acet committed
100
101
      }

102
      return __('Merge');
Fatih Acet's avatar
Fatih Acet committed
103
    },
104
    isRemoveSourceBranchButtonDisabled() {
105
      return this.isMergeButtonDisabled;
106
    },
Fatih Acet's avatar
Fatih Acet committed
107
108
109
110
    shouldShowSquashBeforeMerge() {
      const { commitsCount, enableSquashBeforeMerge } = this.mr;
      return enableSquashBeforeMerge && commitsCount > 1;
    },
111
    shouldShowMergeControls() {
112
      return this.mr.isMergeAllowed || this.isAutoMergeAvailable;
Fatih Acet's avatar
Fatih Acet committed
113
    },
Natalia Tepluhina's avatar
Natalia Tepluhina committed
114
115
116
117
118
119
    shouldShowSquashEdit() {
      return this.squashBeforeMerge && this.shouldShowSquashBeforeMerge;
    },
    shouldShowMergeEdit() {
      return !this.mr.ffOnlyEnabled;
    },
120
121
122
123
124
125
126
127
128
129
130
131
    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,
      );
    },
132
133
134
135
136
  },
  methods: {
    updateMergeCommitMessage(includeDescription) {
      const { commitMessageWithDescription, commitMessage } = this.mr;
      this.commitMessage = includeDescription ? commitMessageWithDescription : commitMessage;
Fatih Acet's avatar
Fatih Acet committed
137
    },
138
139
    handleMergeButtonClick(useAutoMerge, mergeImmediately = false) {
      if (mergeImmediately) {
Fatih Acet's avatar
Fatih Acet committed
140
141
142
143
        this.isMergingImmediately = true;
      }

      const options = {
144
        sha: this.mr.latestSHA || this.mr.sha,
Fatih Acet's avatar
Fatih Acet committed
145
        commit_message: this.commitMessage,
146
        auto_merge_strategy: useAutoMerge ? this.mr.preferredAutoMergeStrategy : undefined,
Fatih Acet's avatar
Fatih Acet committed
147
        should_remove_source_branch: this.removeSourceBranch === true,
148
        squash: this.squashBeforeMerge,
Fatih Acet's avatar
Fatih Acet committed
149
150
      };

151
152
153
154
155
156
157
      // 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;
      }

Fatih Acet's avatar
Fatih Acet committed
158
      this.isMakingRequest = true;
159
160
      this.service
        .merge(options)
161
        .then(res => res.data)
162
        .then(data => {
163
          const hasError = data.status === 'failed' || data.status === 'hook_validation_error';
Fatih Acet's avatar
Fatih Acet committed
164

165
          if (_.includes(AUTO_MERGE_STRATEGIES, data.status)) {
Fatih Acet's avatar
Fatih Acet committed
166
            eventHub.$emit('MRWidgetUpdateRequested');
167
          } else if (data.status === 'success') {
Fatih Acet's avatar
Fatih Acet committed
168
169
            this.initiateMergePolling();
          } else if (hasError) {
170
            eventHub.$emit('FailedToMerge', data.merge_error);
Fatih Acet's avatar
Fatih Acet committed
171
172
173
174
          }
        })
        .catch(() => {
          this.isMakingRequest = false;
175
          new Flash(__('Something went wrong. Please try again.')); // eslint-disable-line
Fatih Acet's avatar
Fatih Acet committed
176
177
        });
    },
178
179
180
181
182
183
184
185
186
187
    handleMergeImmediatelyButtonClick() {
      if (this.isMergeImmediatelyDangerous) {
        this.$refs.confirmationDialog.show();
      } else {
        this.handleMergeButtonClick(false, true);
      }
    },
    onMergeImmediatelyConfirmation() {
      this.handleMergeButtonClick(false, true);
    },
Fatih Acet's avatar
Fatih Acet committed
188
    initiateMergePolling() {
189
190
191
192
193
194
      simplePoll(
        (continuePolling, stopPolling) => {
          this.handleMergePolling(continuePolling, stopPolling);
        },
        { timeout: 0 },
      );
Fatih Acet's avatar
Fatih Acet committed
195
196
    },
    handleMergePolling(continuePolling, stopPolling) {
197
198
      this.service
        .poll()
199
        .then(res => res.data)
200
        .then(data => {
201
          if (data.state === 'merged') {
Fatih Acet's avatar
Fatih Acet committed
202
203
204
            // If state is merged we should update the widget and stop the polling
            eventHub.$emit('MRWidgetUpdateRequested');
            eventHub.$emit('FetchActionsContent');
205
            MergeRequest.setStatusBoxToMerged();
206
207
            MergeRequest.hideCloseButton();
            MergeRequest.decreaseCounter();
Fatih Acet's avatar
Fatih Acet committed
208
209
            stopPolling();

210
211
            refreshUserMergeRequestCounts();

Fatih Acet's avatar
Fatih Acet committed
212
213
            // If user checked remove source branch and we didn't remove the branch yet
            // we should start another polling for source branch remove process
214
            if (this.removeSourceBranch && data.source_branch_exists) {
Fatih Acet's avatar
Fatih Acet committed
215
216
              this.initiateRemoveSourceBranchPolling();
            }
217
218
          } else if (data.merge_error) {
            eventHub.$emit('FailedToMerge', data.merge_error);
Fatih Acet's avatar
Fatih Acet committed
219
220
221
222
223
224
225
            stopPolling();
          } else {
            // MR is not merged yet, continue polling until the state becomes 'merged'
            continuePolling();
          }
        })
        .catch(() => {
226
          new Flash(__('Something went wrong while merging this merge request. Please try again.')); // eslint-disable-line
227
          stopPolling();
Fatih Acet's avatar
Fatih Acet committed
228
229
230
231
232
233
234
235
236
237
238
        });
    },
    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) {
239
240
      this.service
        .poll()
241
        .then(res => res.data)
242
        .then(data => {
Fatih Acet's avatar
Fatih Acet committed
243
244
          // If source branch exists then we should continue polling
          // because removing a source branch is a background task and takes time
245
          if (data.source_branch_exists) {
Fatih Acet's avatar
Fatih Acet committed
246
247
248
249
250
251
252
253
254
255
            continuePolling();
          } else {
            // Branch is removed. Update widget, stop polling and hide the spinner
            eventHub.$emit('MRWidgetUpdateRequested', () => {
              eventHub.$emit('SetBranchRemoveFlag', [false]);
            });
            stopPolling();
          }
        })
        .catch(() => {
256
          new Flash(__('Something went wrong while deleting the source branch. Please try again.')); // eslint-disable-line
Fatih Acet's avatar
Fatih Acet committed
257
258
259
        });
    },
  },
260
261
262
263
};
</script>

<template>
264
265
266
267
268
269
  <div>
    <div class="mr-widget-body media">
      <status-icon :status="iconClass" />
      <div class="media-body">
        <div class="mr-widget-body-controls media space-children">
          <span class="btn-group">
Simon Knox's avatar
Simon Knox committed
270
271
            <button
              :disabled="isMergeButtonDisabled"
272
              :class="mergeButtonClass"
273
              type="button"
274
              class="qa-merge-button"
275
              @click="handleMergeButtonClick(isAutoMergeAvailable)"
Mike Greiling's avatar
Mike Greiling committed
276
            >
277
278
              <i v-if="isMakingRequest" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
              {{ mergeButtonText }}
Simon Knox's avatar
Simon Knox committed
279
            </button>
280
            <button
Nathan Friend's avatar
Nathan Friend committed
281
              v-if="shouldShowMergeImmediatelyDropdown"
282
283
284
285
              :disabled="isMergeButtonDisabled"
              type="button"
              class="btn btn-sm btn-info dropdown-toggle js-merge-moment"
              data-toggle="dropdown"
286
              data-qa-selector="merge_moment_dropdown"
287
              :aria-label="__('Select merge moment')"
288
            >
289
              <i class="fa fa-chevron-down" aria-hidden="true"></i>
290
291
            </button>
            <ul
Nathan Friend's avatar
Nathan Friend committed
292
              v-if="shouldShowMergeImmediatelyDropdown"
293
294
295
296
297
              class="dropdown-menu dropdown-menu-right"
              role="menu"
            >
              <li>
                <a
298
                  class="auto_merge_enabled qa-merge-when-pipeline-succeeds-option"
299
300
301
302
303
                  href="#"
                  @click.prevent="handleMergeButtonClick(true)"
                >
                  <span class="media">
                    <span class="merge-opt-icon" aria-hidden="true" v-html="successSvg"></span>
304
                    <span class="media-body merge-opt-title">{{ autoMergeText }}</span>
305
306
307
308
                  </span>
                </a>
              </li>
              <li>
309
310
311
312
313
                <merge-immediately-confirmation-dialog
                  ref="confirmationDialog"
                  :docs-url="mr.mergeImmediatelyDocsPath"
                  @mergeImmediately="onMergeImmediatelyConfirmation"
                />
314
                <a
315
                  class="accept-merge-request js-merge-immediately-button"
316
                  data-qa-selector="merge_immediately_option"
317
                  href="#"
318
                  @click.prevent="handleMergeImmediatelyButtonClick"
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
                >
                  <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="isMergeButtonDisabled"
              />
            </template>
            <template v-else>
              <span class="bold js-resolve-mr-widget-items-message">
351
                {{ mergeDisabledText }}
352
353
              </span>
            </template>
Fatih Acet's avatar
Fatih Acet committed
354
355
          </div>
        </div>
356
357
358
359
        <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>
Simon Knox's avatar
Simon Knox committed
360
      </div>
Fatih Acet's avatar
Fatih Acet committed
361
    </div>
362
363
364
365
    <template v-if="shouldShowMergeControls">
      <div v-if="mr.ffOnlyEnabled" class="mr-fast-forward-message">
        {{ __('Fast-forward merge without a merge commit') }}
      </div>
Natalia Tepluhina's avatar
Natalia Tepluhina committed
366
367
368
369
370
371
      <commits-header
        v-if="shouldShowSquashEdit || shouldShowMergeEdit"
        :is-squash-enabled="squashBeforeMerge"
        :commits-count="mr.commitsCount"
        :target-branch="mr.targetBranch"
        :is-fast-forward-enabled="mr.ffOnlyEnabled"
372
        :class="{ 'border-bottom': mr.mergeError }"
Natalia Tepluhina's avatar
Natalia Tepluhina committed
373
374
375
376
377
378
379
380
381
382
383
      >
        <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"
384
              v-model="squashCommitMessage"
Natalia Tepluhina's avatar
Natalia Tepluhina committed
385
386
387
388
389
390
391
392
393
394
395
396
397
398
              :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)"
399
              />
Natalia Tepluhina's avatar
Natalia Tepluhina committed
400
401
402
403
404
              {{ __('Include merge request description') }}
            </label>
          </commit-edit>
        </ul>
      </commits-header>
405
    </template>
406
407
  </div>
</template>