Commit 0c3abe3e authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 31053-pipeline-ux

* master:
  Fallback to default pattern when note does not belong to project
  Merge request widget redesign
  Fixed focused test in notes spec
  Fixed UP arrow key not editing last comment in discussion
  Fix skipped manual actions issue in pipeline processing
  Fix notify_only_default_branch check for Slack service
parents d3b124e4 387b4410
......@@ -88,6 +88,7 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
} else {
new Flash(errorFlashMsg);
}
......
......@@ -49,6 +49,7 @@ class ResolveServiceClass {
discussion.resolveAllNotes(resolved_by);
}
gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
} else {
throw new Error('An error occurred when trying to resolve discussion.');
......
......@@ -10,7 +10,6 @@
/* global IssuableForm */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global MergedButtons */
/* global Commit */
/* global NotificationsForm */
/* global TreeView */
......@@ -216,15 +215,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
new gl.Diff();
shortcut_handler = new ShortcutsIssuable(true);
new ZenMode();
new MergedButtons();
break;
case 'projects:merge_requests:commits':
new MergedButtons();
break;
case "projects:merge_requests:diffs":
new gl.Diff();
new ZenMode();
new MergedButtons();
break;
case 'dashboard:activity':
new gl.Activities();
......
export default (fn, interval = 2000, timeout = 60000) => {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => {
if (Date.now() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), interval);
} else {
reject(new Error('SIMPLE_POLL_TIMEOUT'));
}
};
fn(next, stop);
});
};
......@@ -123,8 +123,6 @@ import './member_expiration_date';
import './members';
import './merge_request';
import './merge_request_tabs';
import './merge_request_widget';
import './merged_buttons';
import './milestone';
import './milestone_select';
import './mini_pipeline_graph_dropdown';
......
......@@ -106,6 +106,21 @@ require('./merge_request_tabs');
});
};
MergeRequest.prototype.updateStatusText = function(classToRemove, classToAdd, newStatusText) {
$('.detail-page-header .status-box')
.removeClass(classToRemove)
.addClass(classToAdd)
.find('span')
.text(newStatusText);
};
MergeRequest.prototype.decreaseCounter = function(by = 1) {
const $el = $('.nav-links .js-merge-counter');
const count = Math.max((parseInt($el.text().replace(/[^\d]/, ''), 10) - by), 0);
$el.text(gl.text.addDelimiter(count));
};
return MergeRequest;
})();
}).call(window);
/* global merge_request_widget */
(() => {
$(() => {
/* TODO: This needs a better home, or should be refactored. It was previously contained
* in a script tag in app/views/projects/merge_requests/widget/open/_accept.html.haml,
* but Vue chokes on script tags and prevents their execution. So it was moved here
* temporarily.
* */
$(document)
.off('ajax:send', '.accept-mr-form')
.on('ajax:send', '.accept-mr-form', () => {
$('.accept-mr-form :input').disable();
});
$(document)
.off('click', '.accept-merge-request')
.on('click', '.accept-merge-request', () => {
$('.js-merge-button, .js-merge-when-pipeline-succeeds-button').html('<i class="fa fa-spinner fa-spin"></i> Merge in progress');
});
$(document)
.off('click', '.merge-when-pipeline-succeeds')
.on('click', '.merge-when-pipeline-succeeds', () => {
$('#merge_when_pipeline_succeeds').val('1');
});
$(document)
.off('click', '.js-merge-dropdown a')
.on('click', '.js-merge-dropdown a', (e) => {
e.preventDefault();
$(e.target).closest('form').submit();
});
if ($('.rebase-in-progress').length) {
merge_request_widget.rebaseInProgress();
} else if ($('.rebase-mr-form').length) {
$(document)
.off('ajax:send', '.rebase-mr-form')
.on('ajax:send', '.rebase-mr-form', () => {
$('.rebase-mr-form :input').disable();
});
$(document)
.off('click', '.js-rebase-button')
.on('click', '.js-rebase-button', () => {
$('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress");
});
} else {
setTimeout(() => merge_request_widget.getMergeStatus(), 200);
}
});
})();
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
import '~/lib/utils/url_utility';
(function() {
this.MergedButtons = (function() {
function MergedButtons() {
this.removeSourceBranch = this.removeSourceBranch.bind(this);
this.removeBranchSuccess = this.removeBranchSuccess.bind(this);
this.removeBranchError = this.removeBranchError.bind(this);
this.$removeBranchWidget = $('.remove_source_branch_widget');
this.$removeBranchProgress = $('.remove_source_branch_in_progress');
this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
this.cleanEventListeners();
this.initEventListeners();
}
MergedButtons.prototype.cleanEventListeners = function() {
$(document).off('click', '.remove_source_branch');
$(document).off('ajax:success', '.remove_source_branch');
return $(document).off('ajax:error', '.remove_source_branch');
};
MergedButtons.prototype.initEventListeners = function() {
$(document).on('click', '.remove_source_branch', this.removeSourceBranch);
$(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess);
$(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
};
MergedButtons.prototype.removeSourceBranch = function() {
this.$removeBranchWidget.hide();
return this.$removeBranchProgress.show();
};
MergedButtons.prototype.removeBranchSuccess = function() {
gl.utils.refreshCurrentPage();
};
MergedButtons.prototype.removeBranchError = function() {
this.$removeBranchWidget.hide();
this.$removeBranchProgress.hide();
return this.$removeBranchFailed.show();
};
return MergedButtons;
})();
}).call(window);
......@@ -175,7 +175,7 @@ const normalizeNewlines = function(str) {
if ($textarea.val() !== '') {
return;
}
myLastNote = $("li.note[data-author-id='" + gon.current_user_id + "'][data-editable]:last");
myLastNote = $(`li.note[data-author-id='${gon.current_user_id}'][data-editable]:last`, $textarea.closest('.note, #notes'));
if (myLastNote.length) {
myLastNoteEditBtn = myLastNote.find('.js-note-edit');
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
......@@ -276,7 +276,7 @@ const normalizeNewlines = function(str) {
var votesBlock;
if (noteEntity.commands_changes) {
if ('merge' in noteEntity.commands_changes) {
$.get(mrRefreshWidgetUrl);
Notes.checkMergeRequestStatus();
}
if ('emoji_award' in noteEntity.commands_changes) {
......@@ -424,6 +424,7 @@ const normalizeNewlines = function(str) {
}
gl.utils.localTimeAgo($('.js-timeago'), false);
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
};
......@@ -769,7 +770,8 @@ const normalizeNewlines = function(str) {
}
};
})(this));
// Decrement the "Discussions" counter only once
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
};
......@@ -1115,6 +1117,12 @@ const normalizeNewlines = function(str) {
return $form;
};
Notes.checkMergeRequestStatus = function() {
if (gl.utils.getPagePath(1) === 'merge_requests') {
gl.mrWidget.checkStatus();
}
};
Notes.animateAppendNote = function(noteHtml, $notesList) {
const $note = $(noteHtml);
......
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
export default {
data() {
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
svgHTML() {
return borderlessStatusIconEntityMap[this.stage.status.icon];
},
},
watch: {
'stage.title': function stageTitle() {
$(this.$refs.button).tooltip('destroy').tooltip();
},
},
template: `
<div>
<button
@click="fetchBuilds($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
ref="button"
:aria-label="stage.title">
<span v-html="svgHTML" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
</div>
</ul>
</div>
`,
};
export default {
name: 'MRWidgetAuthor',
props: {
author: { type: Object, required: true },
showAuthorName: { type: Boolean, required: false, default: true },
showAuthorTooltip: { type: Boolean, required: false, default: false },
},
template: `
<a
:href="author.webUrl || author.web_url"
class="author-link"
:class="{ 'has-tooltip': showAuthorTooltip }"
:title="author.name">
<img
:src="author.avatarUrl || author.avatar_url"
class="avatar avatar-inline s16" />
<span
v-if="showAuthorName"
class="author">{{author.name}}
</span>
</a>
`,
};
import MRWidgetAuthor from './mr_widget_author';
export default {
name: 'MRWidgetAuthorTime',
props: {
actionText: { type: String, required: true },
author: { type: Object, required: true },
dateTitle: { type: String, required: true },
dateReadable: { type: String, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
template: `
<h4 class="js-mr-widget-author">
{{actionText}}
<mr-widget-author :author="author" />
<time
:title="dateTitle"
data-toggle="tooltip"
data-placement="top"
data-container="body">
{{dateReadable}}
</time>
</h4>
`,
};
/* global Flash */
import '~/lib/utils/datetime_utility';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
import MemoryUsage from './mr_widget_memory_usage';
import MRWidgetService from '../services/mr_widget_service';
export default {
name: 'MRWidgetDeployment',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-memory-usage': MemoryUsage,
},
computed: {
svg() {
return statusClassToSvgMap.icon_status_success;
},
},
methods: {
formatDate(date) {
return gl.utils.getTimeago().format(date);
},
hasExternalUrls(deployment = {}) {
return deployment.external_url && deployment.external_url_formatted;
},
hasDeploymentTime(deployment = {}) {
return deployment.deployed_at && deployment.deployed_at_formatted;
},
hasDeploymentMeta(deployment = {}) {
return deployment.url && deployment.name;
},
stopEnvironment(deployment) {
const msg = 'Are you sure you want to stop this environment?';
const isConfirmed = confirm(msg); // eslint-disable-line
if (isConfirmed) {
MRWidgetService.stopEnvironment(deployment.stop_url)
.then(res => res.json())
.then((res) => {
if (res.redirect_url) {
gl.utils.visitUrl(res.redirect_url);
}
})
.catch(() => {
new Flash('Something went wrong while stopping this environment. Please try again.'); // eslint-disable-line
});
}
},
},
template: `
<div class="mr-widget-heading">
<div v-for="deployment in mr.deployments">
<div class="ci-widget">
<div class="ci-status-icon ci-status-icon-success">
<span class="js-icon-link icon-link">
<span
v-html="svg"
aria-hidden="true"></span>
</span>
</div>
<span>
<span
v-if="hasDeploymentMeta(deployment)">
Deployed to
</span>
<a
v-if="hasDeploymentMeta(deployment)"
:href="deployment.url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-meta">
{{deployment.name}}
</a>
<span
v-if="hasExternalUrls(deployment)">
on
</span>
<a
v-if="hasExternalUrls(deployment)"
:href="deployment.external_url"
target="_blank"
rel="noopener noreferrer nofollow"
class="js-deploy-url">
<i
class="fa fa-external-link"
aria-hidden="true" />
{{deployment.external_url_formatted}}
</a>
<span
v-if="hasDeploymentTime(deployment)"
:data-title="deployment.deployed_at_formatted"
class="js-deploy-time"
data-toggle="tooltip"
data-placement="top">
{{formatDate(deployment.deployed_at)}}
</span>
<button
type="button"
v-if="deployment.stop_url"
@click="stopEnvironment(deployment)"
class="btn btn-default btn-xs">
Stop environment
</button>
</span>
</div>
<mr-widget-memory-usage
v-if="deployment.metrics_url"
:mr="mr"
:service="service"
:metricsUrl="deployment.metrics_url"
/>
</div>
</div>
`,
};
require('../../lib/utils/text_utility');
export default {
name: 'MRWidgetHeader',
props: {
mr: { type: Object, required: true },
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
template: `
<div class="mr-source-target">
<div
v-if="mr.isOpen"
class="pull-right">
<a
href="#modal_merge_info"
data-toggle="modal"
class="btn inline btn-grouped btn-sm">
Check out branch
</a>
<span class="dropdown inline prepend-left-5">
<a
class="btn btn-sm dropdown-toggle"
data-toggle="dropdown"
aria-label="Download as"
role="button">
<i
class="fa fa-download"
aria-hidden="true" />
<i
class="fa fa-caret-down"
aria-hidden="true" />
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li>
<a
:href="mr.emailPatchesPath"
download>
Email patches
</a>
</li>
<li>
<a
:href="mr.plainDiffPath"
download>
Plain diff
</a>
</li>
</ul>
</span>
</div>
<div class="normal">
<b>Request to merge</b>
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.sourceBranch)}"
:title="isBranchTitleLong(mr.sourceBranch) ? mr.sourceBranch : ''"
data-placement="bottom"
v-html="mr.sourceBranchLink"></span>
<button
class="btn btn-transparent btn-clipboard has-tooltip"
data-title="Copy branch name to clipboard"
:data-clipboard-text="mr.sourceBranch">
<i
aria-hidden="true"
class="fa fa-clipboard"></i>
</button>
<b>into</b>
<span
class="label-branch"
:class="{'label-truncated has-tooltip': isBranchTitleLong(mr.targetBranch)}"
:title="isBranchTitleLong(mr.targetBranch) ? mr.targetBranch : ''"
data-placement="bottom">
<a
:href="mr.targetBranchCommitsPath">
{{mr.targetBranch}}
</a>
</span>
<span
v-if="shouldShowCommitsBehindText"
class="diverged-commits-count">
({{mr.divergedCommitsCount}} {{commitsText}} behind)
</span>
</div>
</div>
`,
};
import statusCodes from '~/lib/utils/http_status';
import MemoryGraph from '../../vue_shared/components/memory_graph';
import MRWidgetService from '../services/mr_widget_service';
export default {
name: 'MemoryUsage',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
metricsUrl: { type: String, required: true },
},
data() {
return {
// memoryFrom: 0,
// memoryTo: 0,
memoryMetrics: [],
hasMetrics: false,
loadFailed: false,
loadingMetrics: true,
backOffRequestCounter: 0,
};
},
components: {
'mr-memory-graph': MemoryGraph,
},
methods: {
computeGraphData(metrics) {
this.loadingMetrics = false;
const { memory_values } = metrics;
// if (memory_previous.length > 0) {
// this.memoryFrom = Number(memory_previous[0].value[1]).toFixed(2);
// }
//
// if (memory_current.length > 0) {
// this.memoryTo = Number(memory_current[0].value[1]).toFixed(2);
// }
if (memory_values.length > 0) {
this.hasMetrics = true;
this.memoryMetrics = memory_values[0].values;
}
},
},
mounted() {
this.$props.loadingMetrics = true;
gl.utils.backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.$props.metricsUrl)
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
} else {
stop(res);
}
})
.catch(stop);
})
.then((res) => {
if (res.status === statusCodes.NO_CONTENT) {
return res;
}
return res.json();
})
.then((res) => {
this.computeGraphData(res.metrics);
return res;
})
.catch(() => {
this.$props.loadFailed = true;
});
},
template: `
<div class="mr-info-list mr-memory-usage">
<div class="legend"></div>
<p
v-if="loadingMetrics"
class="usage-info usage-info-loading">
<i
class="fa fa-spinner fa-spin usage-info-load-spinner"
aria-hidden="true" />Loading deployment statistics.
</p>
<p
v-if="!hasMetrics && !loadingMetrics"
class="usage-info usage-info-loading">
Deployment statistics are not available currently.
</p>
<p
v-if="hasMetrics"
class="usage-info">
Deployment memory usage:
</p>
<p
v-if="loadFailed"
class="usage-info">
Failed to load deployment statistics.
</p>
<mr-memory-graph
v-if="hasMetrics"
:metrics="memoryMetrics"
height="25"
width="100" />
</div>
`,
};
export default {
name: 'MRWidgetMergeHelp',
props: {
missingBranch: { type: String, required: false, default: '' },
},
template: `
<section class="mr-widget-help">
<template
v-if="missingBranch">
If the {{missingBranch}} branch exists in your local repository, you
</template>
<template v-else>
You
</template>
can merge this merge request manually using the
<a
data-toggle="modal"
href="#modal_merge_info">
command line.
</a>
</section>
`,
};
import PipelineStage from '../../pipelines/components/stage';
import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
export default {
name: 'MRWidgetPipeline',
props: {
mr: { type: Object, required: true },
},
components: {
'pipeline-stage': PipelineStage,
'pipeline-status-icon': pipelineStatusIcon,
},
computed: {
hasCIError() {
const { hasCI, ciStatus } = this.mr;
return hasCI && !ciStatus;
},
svg() {
return statusClassToSvgMap.icon_status_failed;
},
stageText() {
return this.mr.pipeline.details.stages.length > 1 ? 'stages' : 'stage';
},
},
template: `
<div class="mr-widget-heading">
<div class="ci-widget">
<template v-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed js-ci-error">
<span class="js-icon-link icon-link">
<span
v-html="svg"
aria-hidden="true"></span>
</span>
</div>
<span>Could not connect to the CI server. Please check your settings and try again.</span>
</template>
<template v-else>
<pipeline-status-icon :pipelineStatus="mr.pipelineDetailedStatus" />
<span>
Pipeline
<a
:href="mr.pipeline.path"
class="pipeline-id">#{{mr.pipeline.id}}</a>
{{mr.pipeline.details.status.label}}
with {{stageText}}
</span>
<div class="mr-widget-pipeline-graph">
<div class="stage-cell">
<div
v-if="mr.pipeline.details.stages.length > 0"
v-for="stage in mr.pipeline.details.stages"
class="stage-container dropdown js-mini-pipeline-graph">
<pipeline-stage :stage="stage" />
</div>
</div>
</div>
<span>
for
<a
:href="mr.pipeline.commit.commit_path"
class="monospace js-commit-link">
{{mr.pipeline.commit.short_id}}</a>.
</span>
<span
v-if="mr.pipeline.coverage"
class="js-mr-coverage">
Coverage {{mr.pipeline.coverage}}%.
</span>
</template>
</div>
</div>
`,
};
export default {
name: 'MRWidgetRelatedLinks',
props: {
relatedLinks: { type: Object, required: true },
},
computed: {
hasLinks() {
const { closing, mentioned, assignToMe } = this.relatedLinks;
return closing || mentioned || assignToMe;
},
},
methods: {
hasMultipleIssues(text) {
return !text ? false : text.match(/<\/a> and <a/);
},
issueLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
},
verbLabel(field) {
return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
},
},
template: `
<section
v-if="hasLinks"
class="mr-info-list mr-links">
<div class="legend"></div>
<p v-if="relatedLinks.closing">
Closes {{issueLabel('closing')}}
<span v-html="relatedLinks.closing"></span>.
</p>
<p v-if="relatedLinks.mentioned">
<span class="capitalize">{{issueLabel('mentioned')}}</span>
<span v-html="relatedLinks.mentioned"></span>
{{verbLabel('mentioned')}} mentioned but will not be closed.
</p>
<p v-if="relatedLinks.assignToMe">
<span v-html="relatedLinks.assignToMe"></span>
</p>
</section>
`,
};
export default {
name: 'MRWidgetArchived',
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
This project is archived, write access has been disabled.
</span>
</div>
`,
};
export default {
name: 'MRWidgetAutoMergeFailed',
props: {
mr: { type: Object, required: true },
},
template: `
<div class="mr-widget-body">
<button
class="btn btn-success btn-small"
disabled="true"
type="button">
Merge
</button>
<span class="bold danger">
This merge request failed to be merged automatically.
</span>
<div class="merge-error-text">
{{mr.mergeError}}
</div>
</div>
`,
};
export default {
name: 'MRWidgetChecking',
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
Checking ability to merge automatically.
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</span>
</div>
`,
};
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
export default {
name: 'MRWidgetClosed',
props: {
mr: { type: Object, required: true },
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
},
template: `
<div class="mr-widget-body">
<mr-widget-author-and-time
actionText="Closed by"
:author="mr.closedBy"
:dateTitle="mr.updatedAt"
:dateReadable="mr.closedAt"
/>
<section>
<p>
The changes were not merged into
<a
:href="mr.targetBranchCommitsPath"
class="label-branch">
{{mr.targetBranch}}</a>.
</p>
</section>
</div>
`,
};
export default {
name: 'MRWidgetConflicts',
props: {
mr: { type: Object, required: true },
},
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
There are merge conflicts.
<span v-if="!mr.canMerge">
Resolve these conflicts or ask someone with write access to this repository to merge it locally.
</span>
</span>
<div
v-if="mr.canMerge"
class="btn-group">
<a
v-if="mr.conflictResolutionPath"
:href="mr.conflictResolutionPath"
class="btn btn-default btn-xs js-resolve-conflicts-button">
Resolve conflicts
</a>
<a
v-if="mr.canMerge"
class="btn btn-default btn-xs js-merge-locally-button"
data-toggle="modal"
href="#modal_merge_info">
Merge locally
</a>
</div>
</div>
`,
};
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetFailedToMerge',
props: {
mr: { type: Object, required: true },
},
data() {
return {
timer: 10,
isRefreshing: false,
};
},
mounted() {
setInterval(() => {
this.updateTimer();
}, 1000);
},
created() {
eventHub.$emit('DisablePolling');
},
computed: {
timerText() {
return this.timer > 1 ? `${this.timer} seconds` : 'a second';
},
},
methods: {
refresh() {
this.isRefreshing = true;
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('EnablePolling');
},
updateTimer() {
this.timer = this.timer - 1;
if (this.timer === 0) {
this.refresh();
}
},
},
template: `
<div class="mr-widget-body">
<button
class="btn btn-success btn-small"
disabled="true"
type="button">
Merge
</button>
<span
v-if="!isRefreshing"
class="bold danger">
<span
class="has-error-message"
v-if="mr.mergeError">
{{mr.mergeError}}
</span>
<span v-else>Merge failed.</span>
<span
:class="{ 'has-custom-error': mr.mergeError }">
Refreshing in {{timerText}} to show the updated status...
</span>
<button
@click="refresh"
class="btn btn-default btn-xs js-refresh-button"
type="button">
Refresh now
</button>
</span>
<span
v-if="isRefreshing"
class="bold js-refresh-label">
Refreshing now...
</span>
</div>
`,
};
export default {
name: 'MRWidgetLocked',
props: {
mr: { type: Object, required: true },
},
template: `
<div class="mr-widget-body mr-state-locked">
<span class="state-label">Locked</span>
This merge request is in the process of being merged, during which time it is locked and cannot be closed.
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
<section class="mr-info-list mr-links">
<div class="legend"></div>
<p>
The changes will be merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</p>
</section>
</div>
`,
};
/* global Flash */
import MRWidgetAuthor from '../../components/mr_widget_author';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMergeWhenPipelineSucceeds',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-author': MRWidgetAuthor,
},
data() {
return {
isCancellingAutoMerge: false,
isRemovingSourceBranch: false,
};
},
computed: {
canRemoveSourceBranch() {
const { shouldRemoveSourceBranch, canRemoveSourceBranch,
mergeUserId, currentUserId } = this.mr;
return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
},
},
methods: {
cancelAutomaticMerge() {
this.isCancellingAutoMerge = true;
this.service.cancelAutomaticMerge()
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
})
.catch(() => {
this.isCancellingAutoMerge = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
removeSourceBranch() {
const options = {
sha: this.mr.sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
};
this.isRemovingSourceBranch = true;
this.service.mergeResource.save(options)
.then(res => res.json())
.then((res) => {
if (res.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
}
})
.catch(() => {
this.isRemovingSourceBranch = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
template: `
<div class="mr-widget-body">
<h4>
Set by
<mr-widget-author :author="mr.setToMWPSBy" />
to be merged automatically when the pipeline succeeds.
<a
v-if="mr.canCancelAutomaticMerge"
@click.prevent="cancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-xs btn-default js-cancel-auto-merge">
<i
v-if="isCancellingAutoMerge"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Cancel automatic merge
</a>
</h4>
<section class="mr-info-list">
<div class="legend"></div>
<p>The changes will be merged into
<a
:href="mr.targetBranchPath"
class="label-branch">
{{mr.targetBranch}}
</a>
</p>
<p v-if="mr.shouldRemoveSourceBranch">
The source branch will be removed.
</p>
<p
v-else
class="with-button">
The source branch will not be removed.
<a
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
@click.prevent="removeSourceBranch"
role="button"
class="btn btn-xs btn-default js-remove-source-branch"
href="#">
<i
v-if="isRemovingSourceBranch"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Remove source branch
</a>
</p>
</section>
</div>
`,
};
/* global Flash */
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
},
data() {
return {
isMakingRequest: false,
};
},
computed: {
shouldShowRemoveSourceBranch() {
const { sourceBranchRemoved, isRemovingSourceBranch, canRemoveSourceBranch } = this.mr;
return !sourceBranchRemoved && canRemoveSourceBranch &&
!this.isMakingRequest && !isRemovingSourceBranch;
},
shouldShowSourceBranchRemoving() {
const { sourceBranchRemoved, isRemovingSourceBranch } = this.mr;
return !sourceBranchRemoved && (isRemovingSourceBranch || this.isMakingRequest);
},
shouldShowMergedButtons() {
const { canRevertInCurrentMR, canCherryPickInCurrentMR, revertInForkPath,
cherryPickInForkPath } = this.mr;
return canRevertInCurrentMR || canCherryPickInCurrentMR ||
revertInForkPath || cherryPickInForkPath;
},
},
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.json())
.then((res) => {
if (res.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
}
})
.catch(() => {
this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
template: `
<div class="mr-widget-body">
<mr-widget-author-and-time
actionText="Merged by"
:author="mr.mergedBy"
:dateTitle="mr.updatedAt"
:dateReadable="mr.mergedAt" />
<section class="mr-info-list">
<div class="legend"></div>
<p>
The changes were merged into
<span class="label-branch">
<a :href="mr.targetBranchPath">{{mr.targetBranch}}</a>
</span>
</p>
<p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p>
<p v-if="shouldShowRemoveSourceBranch">
You can remove source branch now.
<button
@click="removeSourceBranch"
:class="{ disabled: isMakingRequest }"
type="button"
class="btn btn-xs btn-default js-remove-branch-button">
Remove Source Branch
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true" />
The source branch is being removed.
</p>
</section>
<div
v-if="shouldShowMergedButtons"
class="merged-buttons clearfix">
<a
v-if="mr.canRevertInCurrentMR"
class="btn btn-close btn-sm has-tooltip"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
title="Revert this merge request in a new merge request">
Revert
</a>
<a
v-else-if="mr.revertInForkPath"
class="btn btn-close btn-sm has-tooltip"
data-method="post"
:href="mr.revertInForkPath"
title="Revert this merge request in a new merge request">
Revert
</a>
<a
v-if="mr.canCherryPickInCurrentMR"
class="btn btn-default btn-sm has-tooltip"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
title="Cherry-pick this merge request in a new merge request">
Cherry-pick
</a>
<a
v-else-if="mr.cherryPickInForkPath"
class="btn btn-default btn-sm has-tooltip"
data-method="post"
:href="mr.cherryPickInForkPath"
title="Cherry-pick this merge request in a new merge request">
Cherry-pick
</a>
</div>
</div>
`,
};
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
export default {
name: 'MRWidgetMissingBranch',
props: {
mr: { type: Object, required: true },
},
components: {
'mr-widget-merge-help': mrWidgetMergeHelp,
},
computed: {
missingBranchName() {
return this.mr.sourceBranchRemoved ? 'source' : 'target';
},
},
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold js-branch-text">
<span class="capitalize">
{{missingBranchName}}
</span> branch does not exist.
Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch.
</span>
<mr-widget-merge-help
:missing-branch="missingBranchName" />
</div>
`,
};
export default {
name: 'MRWidgetNotAllowed',
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
Ready to be merged automatically.
Ask someone with write access to this repository to merge this request.
</span>
</div>
`,
};
export default {
name: 'MRWidgetNothingToMerge',
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
There is nothing to merge from source branch into target branch.
Please push new commits or use a different branch.
</span>
</div>
`,
};
export default {
name: 'MRWidgetPipelineBlocked',
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.
</span>
</div>
`,
};
export default {
name: 'MRWidgetPipelineBlocked',
template: `
<div class="mr-widget-body">
<button
class="btn btn-success btn-small"
disabled="true"
type="button">
Merge
</button>
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
</span>
</div>
`,
};
/* global Flash */
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import simplePoll from '~/lib/utils/simple_poll';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetReadyToMerge',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
removeSourceBranch: true,
mergeWhenBuildSucceeds: false,
useCommitMessageWithDescription: false,
setToMergeWhenPipelineSucceeds: false,
showCommitMessageEditor: false,
isMakingRequest: false,
isMergingImmediately: false,
commitMessage: this.mr.commitMessage,
successSvg,
warningSvg,
};
},
computed: {
commitMessageLinkTitle() {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
},
mergeButtonClass() {
const defaultClass = 'btn btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
if (hasCI && !ciStatus) {
return failedClass;
} else if (!pipeline) {
return defaultClass;
} else if (isPipelineActive) {
return inActionClass;
} else if (isPipelineFailed) {
return failedClass;
}
return defaultClass;
},
mergeButtonText() {
if (this.isMergingImmediately) {
return 'Merge in progress';
} else if (this.mr.isPipelineActive) {
return 'Merge when pipeline succeeds';
}
return 'Merge';
},
shouldShowMergeOptionsDropdown() {
return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
},
isMergeButtonDisabled() {
const { commitMessage } = this;
return Boolean(!commitMessage.length
|| !this.isMergeAllowed()
|| this.isMakingRequest
|| this.mr.preventMerge);
},
shouldShowSquashBeforeMerge() {
const { commitsCount, enableSquashBeforeMerge } = this.mr;
return enableSquashBeforeMerge && commitsCount > 1;
},
},
methods: {
isMergeAllowed() {
return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed);
},
updateCommitMessage() {
const cmwd = this.mr.commitMessageWithDescription;
this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription;
this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage;
},
toggleCommitMessageEditor() {
this.showCommitMessageEditor = !this.showCommitMessageEditor;
},
handleMergeButtonClick(mergeWhenBuildSucceeds, mergeImmediately) {
// TODO: Remove no-param-reassign
if (mergeWhenBuildSucceeds === undefined) {
mergeWhenBuildSucceeds = this.mr.isPipelineActive; // eslint-disable-line no-param-reassign
} else if (mergeImmediately) {
this.isMergingImmediately = true;
}
this.setToMergeWhenPipelineSucceeds = mergeWhenBuildSucceeds === true;
const options = {
sha: this.mr.sha,
commit_message: this.commitMessage,
merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
should_remove_source_branch: this.removeSourceBranch === true,
};
// Only truthy in EE extension of this component
if (this.setAdditionalParams) {
this.setAdditionalParams(options);
}
this.isMakingRequest = true;
this.service.merge(options)
.then(res => res.json())
.then((res) => {
const hasError = res.status === 'failed' || res.status === 'hook_validation_error';
if (res.status === 'merge_when_pipeline_succeeds') {
eventHub.$emit('MRWidgetUpdateRequested');
} else if (res.status === 'success') {
this.initiateMergePolling();
} else if (hasError) {
eventHub.$emit('FailedToMerge', res.merge_error);
}
})
.catch(() => {
this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
initiateMergePolling() {
simplePoll((continuePolling, stopPolling) => {
this.handleMergePolling(continuePolling, stopPolling);
});
},
handleMergePolling(continuePolling, stopPolling) {
this.service.poll()
.then(res => res.json())
.then((res) => {
if (res.state === 'merged') {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('FetchActionsContent');
if (window.mergeRequest) {
window.mergeRequest.updateStatusText('status-box-open', 'status-box-merged', 'Merged');
window.mergeRequest.decreaseCounter();
}
stopPolling();
// 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 && res.source_branch_exists) {
this.initiateRemoveSourceBranchPolling();
}
} else if (res.merge_error) {
eventHub.$emit('FailedToMerge', res.merge_error);
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
});
},
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.json())
.then((res) => {
// If source branch exists then we should continue polling
// because removing a source branch is a background task and takes time
if (res.source_branch_exists) {
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 removing the source branch. Please try again.'); // eslint-disable-line
});
},
},
template: `
<div class="mr-widget-body">
<span class="btn-group">
<button
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
type="button">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
{{mergeButtonText}}
</button>
<button
v-if="shouldShowMergeOptionsDropdown"
:disabled="isMergeButtonDisabled"
type="button"
class="btn btn-info dropdown-toggle"
data-toggle="dropdown">
<i
class="fa fa-caret-down"
aria-hidden="true" />
<span class="sr-only">
Select merge moment
</span>
</button>
<ul
v-if="shouldShowMergeOptionsDropdown"
class="dropdown-menu dropdown-menu-right"
role="menu">
<li>
<a
@click.prevent="handleMergeButtonClick(true)"
class="merge_when_pipeline_succeeds"
href="#">
<span
v-html="successSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="merge-opt-title">Merge when pipeline succeeds</span>
</a>
</li>
<li>
<a
@click.prevent="handleMergeButtonClick(false, true)"
class="accept-merge-request"
href="#">
<span
v-html="warningSvg"
class="merge-opt-icon"
aria-hidden="true"></span>
<span class="merge-opt-title">Merge immediately</span>
</a>
</li>
</ul>
</span>
<template v-if="isMergeAllowed()">
<label class="spacing">
<input
v-model="removeSourceBranch"
:disabled="isMergeButtonDisabled"
type="checkbox"/> Remove source branch
</label>
<!-- Placeholder for EE extension of this component -->
<squash-before-merge
v-if="shouldShowSquashBeforeMerge"
:mr="mr"
:is-merge-button-disabled="isMergeButtonDisabled" />
<button
@click="toggleCommitMessageEditor"
:disabled="isMergeButtonDisabled"
class="btn btn-default btn-xs"
type="button">
Modify commit message
</button>
<div
v-if="showCommitMessageEditor"
class="prepend-top-default commit-message-editor">
<div class="form-group clearfix">
<label
class="control-label"
for="commit-message">
Commit message
</label>
<div class="col-sm-10">
<div class="commit-message-container">
<div class="max-width-marker"></div>
<textarea
v-model="commitMessage"
class="form-control js-commit-message"
required="required"
rows="14"
name="Commit message"></textarea>
</div>
<p class="hint">Try to keep the first line under 52 characters and the others under 72.</p>
<div class="hint">
<a
@click.prevent="updateCommitMessage"
href="#">{{commitMessageLinkTitle}}</a>
</div>
</div>
</div>
</div>
</template>
<template v-else>
<span class="bold">
The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
</span>
</template>
</div>
`,
};
/*
The squash-before-merge button is EE only, but it's located right in the middle
of the readyToMerge state component template.
If we didn't declare this component in CE, we'd need to maintain a separate copy
of the readyToMergeState template in EE, which is pretty big and likely to change.
Instead, in CE, we declare the component, but it's hidden and is configured to do nothing.
In EE, the configuration extends this object to add a functioning squash-before-merge
button.
*/
export default {
template: '',
};
export default {
name: 'MRWidgetUnresolvedDiscussions',
props: {
mr: { type: Object, required: true },
},
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge
</button>
<span class="bold">
There are unresolved discussions. Please resolve these discussions
<span v-if="mr.canCreateIssue">or</span>
<span v-else>.</span>
</span>
<a
v-if="mr.createIssueToResolveDiscussionsPath"
:href="mr.createIssueToResolveDiscussionsPath"
class="btn btn-default btn-xs js-create-issue">
Create an issue to resolve them later
</a>
</div>
`,
};
/* global Flash */
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetWIP',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
isMakingRequest: false,
};
},
methods: {
removeWIP() {
this.isMakingRequest = true;
this.service.removeWIP()
.then(res => res.json())
.then((res) => {
eventHub.$emit('UpdateWidgetData', res);
new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
$('.merge-request .detail-page-description .title').text(this.mr.title);
})
.catch(() => {
this.isMakingRequest = false;
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
},
template: `
<div class="mr-widget-body">
<button
type="button"
class="btn btn-success btn-small"
disabled="true">
Merge</button>
<span class="bold">
This merge request is currently Work In Progress and therefore unable to merge
</span>
<template v-if="mr.removeWIPPath">
<i
class="fa fa-question-circle has-tooltip"
title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged." />
<button
@click="removeWIP"
:disabled="isMakingRequest"
type="button"
class="btn btn-default btn-xs js-remove-wip">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
Resolve WIP status
</button>
</template>
</div>
`,
};
/**
* This file is the centerpiece of an attempt to reduce potential conflicts
* between the CE and EE versions of the MR widget. EE additions to the MR widget should
* be contained in the ./vue_merge_request_widget/ee directory, and should **extend**
* rather than mutate CE MR Widget code.
*
* This file should be the only source of conflicts between EE and CE. EE-only components should
* imported directly where they are needed, and import paths for EE extensions of CE components
* should overwrite import paths **without** changing the order of dependencies listed here.
*/
export { default as Vue } from 'vue';
export { default as SmartInterval } from '~/smart_interval';
export { default as WidgetHeader } from './components/mr_widget_header';
export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetPipeline } from './components/mr_widget_pipeline';
export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge';
export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as LockedState } from './components/states/mr_widget_locked';
export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived';
export { default as ConflictsState } from './components/states/mr_widget_conflicts';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
export { default as ReadyToMergeState } from './components/states/mr_widget_ready_to_merge';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds';
export { default as AutoMergeFailed } from './components/states/mr_widget_auto_merge_failed';
export { default as CheckingState } from './components/states/mr_widget_checking';
export { default as MRWidgetStore } from './stores/mr_widget_store';
export { default as MRWidgetService } from './services/mr_widget_service';
export { default as eventHub } from './event_hub';
export { default as getStateKey } from './stores/get_state_key';
export { default as mrWidgetOptions } from './mr_widget_options';
export { default as stateMaps } from './stores/state_maps';
export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge';
import Vue from 'vue';
export default new Vue();
import {
Vue,
mrWidgetOptions,
} from './dependencies';
document.addEventListener('DOMContentLoaded', () => {
const vm = new Vue(mrWidgetOptions);
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
};
});
/* global Flash */
import {
WidgetHeader,
WidgetMergeHelp,
WidgetPipeline,
WidgetDeployment,
WidgetRelatedLinks,
MergedState,
ClosedState,
LockedState,
WipState,
ArchivedState,
ConflictsState,
NothingToMergeState,
MissingBranchState,
NotAllowedState,
ReadyToMergeState,
UnresolvedDiscussionsState,
PipelineBlockedState,
PipelineFailedState,
FailedToMerge,
MergeWhenPipelineSucceedsState,
AutoMergeFailed,
CheckingState,
MRWidgetStore,
MRWidgetService,
eventHub,
stateMaps,
SquashBeforeMerge,
} from './dependencies';
export default {
el: '#js-vue-mr-widget',
name: 'MRWidget',
data() {
const store = new MRWidgetStore(gl.mrWidgetData);
const service = this.createService(store);
return {
mr: store,
service,
};
},
computed: {
componentName() {
return stateMaps.stateToComponentMap[this.mr.state];
},
shouldRenderMergeHelp() {
return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1;
},
shouldRenderPipelines() {
return Object.keys(this.mr.pipeline).length || this.mr.hasCI;
},
shouldRenderRelatedLinks() {
return this.mr.relatedLinks;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
},
},
methods: {
createService(store) {
const endpoints = {
mergePath: store.mergePath,
mergeCheckPath: store.mergeCheckPath,
cancelAutoMergePath: store.cancelAutoMergePath,
removeWIPPath: store.removeWIPPath,
sourceBranchPath: store.sourceBranchPath,
ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath,
statusPath: store.statusPath,
mergeActionsContentPath: store.mergeActionsContentPath,
};
return new MRWidgetService(endpoints);
},
checkStatus(cb) {
this.service.checkStatus()
.then(res => res.json())
.then((res) => {
this.mr.setData(res);
this.setFavicon();
if (cb) {
cb.call(null, res);
}
})
.catch(() => {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
initPolling() {
this.pollingInterval = new gl.SmartInterval({
callback: this.checkStatus,
startingInterval: 10000,
maxInterval: 30000,
hiddenInterval: 120000,
incrementByFactorOf: 5000,
});
},
initDeploymentsPolling() {
this.deploymentsInterval = new gl.SmartInterval({
callback: this.fetchDeployments,
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
},
setFavicon() {
if (this.mr.ciStatusFaviconPath) {
gl.utils.setFavicon(this.mr.ciStatusFaviconPath);
}
},
fetchDeployments() {
this.service.fetchDeployments()
.then(res => res.json())
.then((res) => {
if (res.length) {
this.mr.deployments = res;
}
})
.catch(() => {
new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line
});
},
fetchActionsContent() {
this.service.fetchMergeActionsContent()
.then((res) => {
if (res.body) {
const el = document.createElement('div');
el.innerHTML = res.body;
document.body.appendChild(el);
}
})
.catch(() => {
new Flash('Something went wrong. Please try again.'); // eslint-disable-line
});
},
resumePolling() {
this.pollingInterval.resume();
},
stopPolling() {
this.pollingInterval.stopTimer();
},
bindEventHubListeners() {
eventHub.$on('MRWidgetUpdateRequested', (cb) => {
this.checkStatus(cb);
});
// `params` should be an Array contains a Boolean, like `[true]`
// Passing parameter as Boolean didn't work.
eventHub.$on('SetBranchRemoveFlag', (params) => {
this.mr.isRemovingSourceBranch = params[0];
});
eventHub.$on('FailedToMerge', (mergeError) => {
this.mr.state = 'failedToMerge';
this.mr.mergeError = mergeError;
});
eventHub.$on('UpdateWidgetData', (data) => {
this.mr.setData(data);
});
eventHub.$on('FetchActionsContent', () => {
this.fetchActionsContent();
});
eventHub.$on('EnablePolling', () => {
this.resumePolling();
});
eventHub.$on('DisablePolling', () => {
this.stopPolling();
});
},
handleMounted() {
this.checkStatus();
this.setFavicon();
this.initDeploymentsPolling();
},
},
created() {
this.initPolling();
this.bindEventHubListeners();
},
mounted() {
this.handleMounted();
},
components: {
'mr-widget-header': WidgetHeader,
'mr-widget-merge-help': WidgetMergeHelp,
'mr-widget-pipeline': WidgetPipeline,
'mr-widget-deployment': WidgetDeployment,
'mr-widget-related-links': WidgetRelatedLinks,
'mr-widget-merged': MergedState,
'mr-widget-closed': ClosedState,
'mr-widget-locked': LockedState,
'mr-widget-failed-to-merge': FailedToMerge,
'mr-widget-wip': WipState,
'mr-widget-archived': ArchivedState,
'mr-widget-conflicts': ConflictsState,
'mr-widget-nothing-to-merge': NothingToMergeState,
'mr-widget-not-allowed': NotAllowedState,
'mr-widget-missing-branch': MissingBranchState,
'mr-widget-ready-to-merge': ReadyToMergeState,
'mr-widget-squash-before-merge': SquashBeforeMerge,
'mr-widget-checking': CheckingState,
'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
'mr-widget-pipeline-blocked': PipelineBlockedState,
'mr-widget-pipeline-failed': PipelineFailedState,
'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
'mr-widget-auto-merge-failed': AutoMergeFailed,
},
template: `
<div class="mr-state-widget prepend-top-default">
<mr-widget-header :mr="mr" />
<mr-widget-pipeline
v-if="shouldRenderPipelines"
:mr="mr" />
<mr-widget-deployment
v-if="shouldRenderDeployments"
:mr="mr"
:service="service" />
<component
:is="componentName"
:mr="mr"
:service="service" />
<mr-widget-related-links
v-if="shouldRenderRelatedLinks"
:related-links="mr.relatedLinks" />
<mr-widget-merge-help v-if="shouldRenderMergeHelp" />
</div>
`,
};
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class MRWidgetService {
constructor(endpoints) {
this.mergeResource = Vue.resource(endpoints.mergePath);
this.mergeCheckResource = Vue.resource(endpoints.mergeCheckPath);
this.cancelAutoMergeResource = Vue.resource(endpoints.cancelAutoMergePath);
this.removeWIPResource = Vue.resource(endpoints.removeWIPPath);
this.removeSourceBranchResource = Vue.resource(endpoints.sourceBranchPath);
this.deploymentsResource = Vue.resource(endpoints.ciEnvironmentsStatusPath);
this.pollResource = Vue.resource(`${endpoints.statusPath}?basic=true`);
this.mergeActionsContentResource = Vue.resource(endpoints.mergeActionsContentPath);
}
merge(data) {
return this.mergeResource.save(data);
}
cancelAutomaticMerge() {
return this.cancelAutoMergeResource.save();
}
removeWIP() {
return this.removeWIPResource.save();
}
removeSourceBranch() {
return this.removeSourceBranchResource.delete();
}
fetchDeployments() {
return this.deploymentsResource.get();
}
poll() {
return this.pollResource.get();
}
checkStatus() {
return this.mergeCheckResource.get();
}
fetchMergeActionsContent() {
return this.mergeActionsContentResource.get();
}
static stopEnvironment(url) {
return Vue.http.post(url);
}
static fetchMetrics(metricsUrl) {
return Vue.http.get(`${metricsUrl}.json`);
}
}
export default function deviseState(data) {
if (data.project_archived) {
return 'archived';
} else if (data.branch_missing) {
return 'missingBranch';
} else if (!data.commits_count) {
return 'nothingToMerge';
} else if (this.mergeStatus === 'unchecked') {
return 'checking';
} else if (data.has_conflicts) {
return 'conflicts';
} else if (data.work_in_progress) {
return 'workInProgress';
} else if (this.mergeWhenPipelineSucceeds) {
return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
} else if (!this.canMerge) {
return 'notAllowedToMerge';
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
return 'pipelineFailed';
} else if (this.hasMergeableDiscussionsState) {
return 'unresolvedDiscussions';
} else if (this.isPipelineBlocked) {
return 'pipelineBlocked';
} else if (this.canBeMerged) {
return 'readyToMerge';
}
return null;
}
import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
export default class MergeRequestStore {
constructor(data) {
this.setData(data);
}
setData(data) {
const currentUser = data.current_user;
const pipelineStatus = data.pipeline ? data.pipeline.details.status : null;
this.title = data.title;
this.targetBranch = data.target_branch;
this.sourceBranch = data.source_branch;
this.mergeStatus = data.merge_status;
this.sha = data.diff_head_sha;
this.commitMessage = data.merge_commit_message;
this.commitMessageWithDescription = data.merge_commit_message_with_description;
this.commitsCount = data.commits_count;
this.divergedCommitsCount = data.diverged_commits_count;
this.pipeline = data.pipeline || {};
this.deployments = this.deployments || data.deployments || [];
if (data.issues_links) {
const links = data.issues_links;
const { closing } = links;
const mentioned = links.mentioned_but_not_closing;
const assignToMe = links.assign_to_closing;
if (closing || mentioned || assignToMe) {
this.relatedLinks = { closing, mentioned, assignToMe };
}
}
this.updatedAt = data.updated_at;
this.mergedAt = MergeRequestStore.getEventDate(data.merge_event);
this.closedAt = MergeRequestStore.getEventDate(data.closed_event);
this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
this.mergeUserId = data.merge_user_id;
this.currentUserId = gon.current_user_id;
this.sourceBranchPath = data.source_branch_path;
this.sourceBranchLink = data.source_branch_with_namespace_link;
this.mergeError = data.merge_error;
this.targetBranchPath = data.target_branch_commits_path;
this.conflictResolutionPath = data.conflict_resolution_path;
this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
this.removeWIPPath = data.remove_wip_path;
this.sourceBranchRemoved = !data.source_branch_exists;
this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false;
this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
this.mergePath = data.merge_path;
this.statusPath = data.status_path;
this.emailPatchesPath = data.email_patches_path;
this.plainDiffPath = data.plain_diff_path;
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
this.mergeCheckPath = data.merge_check_path;
this.mergeActionsContentPath = data.commit_change_content_path;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
this.canMerge = !!data.merge_path;
this.canCreateIssue = currentUser.can_create_issue || false;
this.canCancelAutomaticMerge = !!data.cancel_merge_when_pipeline_succeeds_path;
this.canBeMerged = data.can_be_merged || false;
// Cherry-pick and Revert actions related
this.canCherryPickInCurrentMR = currentUser.can_cherry_pick_on_current_merge_request || false;
this.canRevertInCurrentMR = currentUser.can_revert_on_current_merge_request || false;
this.cherryPickInForkPath = currentUser.cherry_pick_in_fork_path;
this.revertInForkPath = currentUser.revert_in_fork_path;
// CI related
this.ciEnvironmentsStatusPath = data.ci_environments_status_path;
this.hasCI = data.has_ci;
this.ciStatus = data.ci_status;
this.isPipelineFailed = this.ciStatus ? (this.ciStatus === 'failed' || this.ciStatus === 'canceled') : false;
this.pipelineDetailedStatus = pipelineStatus;
this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false;
this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null;
this.setState(data);
}
setState(data) {
if (this.isOpen) {
this.state = getStateKey.call(this, data);
} else {
switch (data.state) {
case 'merged':
this.state = 'merged';
break;
case 'closed':
this.state = 'closed';
break;
case 'locked':
this.state = 'locked';
break;
default:
this.state = null;
}
}
}
static getAuthorObject(event) {
if (!event) {
return {};
}
return {
name: event.author.name || '',
username: event.author.username || '',
webUrl: event.author.web_url || '',
avatarUrl: event.author.avatar_url || '',
};
}
static getEventDate(event) {
const timeagoInstance = new Timeago();
if (!event) {
return '';
}
return timeagoInstance.format(event.updated_at);
}
}
const stateToComponentMap = {
merged: 'mr-widget-merged',
closed: 'mr-widget-closed',
locked: 'mr-widget-locked',
conflicts: 'mr-widget-conflicts',
missingBranch: 'mr-widget-missing-branch',
workInProgress: 'mr-widget-wip',
readyToMerge: 'mr-widget-ready-to-merge',
nothingToMerge: 'mr-widget-nothing-to-merge',
notAllowedToMerge: 'mr-widget-not-allowed',
archived: 'mr-widget-archived',
checking: 'mr-widget-checking',
unresolvedDiscussions: 'mr-widget-unresolved-discussions',
pipelineBlocked: 'mr-widget-pipeline-blocked',
pipelineFailed: 'mr-widget-pipeline-failed',
mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
failedToMerge: 'mr-widget-failed-to-merge',
autoMergeFailed: 'mr-widget-auto-merge-failed',
};
const statesToShowHelpWidget = [
'locked',
'conflicts',
'workInProgress',
'readyToMerge',
'checking',
'unresolvedDiscussions',
'pipelineFailed',
'pipelineBlocked',
'autoMergeFailed',
];
export default {
stateToComponentMap,
statesToShowHelpWidget,
};
export default {
name: 'MemoryGraph',
props: {
metrics: { type: Array, required: true },
width: { type: String, required: true },
height: { type: String, required: true },
},
data() {
return {
pathD: '',
pathViewBox: '',
// dotX: '',
// dotY: '',
};
},
mounted() {
const renderData = this.$props.metrics.map(v => v[1]);
const maxMemory = Math.max.apply(null, renderData);
const minMemory = Math.min.apply(null, renderData);
const diff = maxMemory - minMemory;
// const cx = 0;
// const cy = 0;
const lineWidth = renderData.length;
const linePath = renderData.map((y, x) => `${x} ${maxMemory - y}`);
this.pathD = `M ${linePath}`;
this.pathViewBox = `0 0 ${lineWidth} ${diff}`;
},
template: `
<div class="memory-graph-container">
<svg :width="width" :height="height" xmlns="http://www.w3.org/2000/svg">
<path :d="pathD" :viewBox="pathViewBox" />
<!--<circle r="0.8" :cx="dotX" :cy="dotY" tranform="translate(0 -1)" /> -->
</svg>
</div>
`,
};
import { statusClassToSvgMap } from '../pipeline_svg_icons';
export default {
name: 'PipelineStatusIcon',
props: {
pipelineStatus: { type: Object, required: true, default: () => ({}) },
},
computed: {
svg() {
return statusClassToSvgMap[this.pipelineStatus.icon];
},
statusClass() {
return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
},
},
template: `
<div :class="statusClass">
<a class="icon-link" :href="pipelineStatus.details_path">
<span v-html="svg" aria-hidden="true"></span>
</a>
</div>
`,
};
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
export const statusClassToSvgMap = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
export const statusClassToBorderlessSvgMap = {
icon_status_canceled: canceledBorderlessSvg,
icon_status_created: createdBorderlessSvg,
icon_status_failed: failedBorderlessSvg,
icon_status_manual: manualBorderlessSvg,
icon_status_pending: pendingBorderlessSvg,
icon_status_running: runningBorderlessSvg,
icon_status_skipped: skippedBorderlessSvg,
icon_status_success: successBorderlessSvg,
icon_status_warning: warningBorderlessSvg,
};
......@@ -47,3 +47,4 @@
@import "framework/emoji-sprites.scss";
@import "framework/icons.scss";
@import "framework/snippets.scss";
@import "framework/memory_graph.scss";
......@@ -92,7 +92,8 @@ hr {
.item-title { font-weight: 600; }
/** FLASH message **/
.author_link {
.author_link,
.author-link {
color: $gl-link-color;
}
......
.ci-status-icon-success {
.ci-status-icon-success,
.ci-status-icon-passed {
color: $green-500;
svg {
......
.memory-graph-container {
svg {
background: $white-light;
}
path {
fill: none;
stroke: $blue-500;
stroke-width: 1px;
}
circle {
stroke: $blue-700;
fill: $blue-700;
}
}
......@@ -111,6 +111,7 @@ $gl-link-hover-color: $blue-800;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c;
$gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343;
$placeholder-text-color: rgba(0, 0, 0, .42);
......
......@@ -37,12 +37,6 @@
@include btn-red;
}
}
.dropdown-toggle {
.fa {
color: inherit;
}
}
}
.accept-control {
......@@ -88,13 +82,13 @@
}
}
.ci_widget {
border-bottom: 1px solid $well-inner-border;
.ci-widget {
color: $gl-text-color;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
padding: $gl-padding-top $gl-padding 0;
i,
svg {
......@@ -115,16 +109,15 @@
flex-wrap: wrap;
}
.ci-status-icon > .icon-link > svg {
.ci-status-icon > .icon-link svg {
width: 22px;
height: 22px;
}
}
.mr-widget-body,
.ci_widget,
.mr-widget-footer {
padding: 16px;
margin: 16px;
}
.mr-widget-pipeline-graph {
......@@ -166,12 +159,41 @@
.normal {
color: $gl-text-color;
font-size: 15px;
}
.capitalize {
text-transform: capitalize;
}
.js-deployment-link {
display: inline-block;
}
.mr-widget-help {
margin: $gl-padding;
color: $ci-skipped-color;
}
.mr-info-list {
&.mr-links {
margin-left: 28px;
}
&.mr-memory-usage {
margin-top: 10px;
margin-bottom: 10px;
}
}
.mr-widget-heading,
.mr-widget-body {
.btn-default.btn-xs {
margin-left: 5px;
}
}
.mr-widget-body {
h4 {
font-weight: 600;
......@@ -182,6 +204,10 @@
&.has-conflicts .fa-exclamation-triangle {
color: $gl-warning;
}
time {
font-weight: normal;
}
}
.btn-grouped {
......@@ -189,6 +215,80 @@
margin-right: 7px;
}
label {
font-weight: normal;
}
.spacing {
margin: 0 $gl-padding;
}
.bold {
margin-left: 5px;
font-weight: bold;
color: $gl-gray-light;
}
.state-label {
font-size: 16px;
font-weight: bold;
padding-right: 10px;
}
.danger {
color: $gl-danger;
}
.mr-widget-help {
margin: $gl-padding 0;
}
.with-button {
position: relative;
top: 6px;
margin-bottom: 24px;
}
.dropdown-menu {
li a {
padding: 5px;
}
.merge-opt-icon,
.merge-opt-title {
display: inline-block;
float: left;
}
.merge-opt-icon svg {
height: 15px;
width: 15px;
}
.merge-opt-title {
margin-left: 8px;
}
}
.dropdown-toggle {
.fa {
color: inherit;
}
}
.has-error-message + .has-custom-error {
margin-left: 0;
}
.has-custom-error {
display: inline-block;
margin-left: 70px;
}
.merge-error-text {
margin-left: 70px;
}
@media (max-width: $screen-xs-max) {
h4 {
font-size: 14px;
......@@ -220,6 +320,17 @@
margin: 0;
}
}
.commit-message-editor {
label {
padding: 0;
}
}
&.mr-state-locked .mr-info-list {
margin-top: 10px;
margin-left: 12px;
}
}
.mr-widget-footer {
......@@ -263,6 +374,24 @@
font-size: 90%;
margin: 0 3px;
word-break: break-all;
&.label-truncated {
position: relative;
display: inline-block;
width: 250px;
margin-bottom: -3px;
white-space: nowrap;
text-overflow: clip;
line-height: 14px;
&::after {
position: absolute;
content: '...';
right: 0;
font-family: $regular_font;
background-color: $gray-light;
}
}
}
.commits-empty {
......@@ -343,61 +472,74 @@
}
}
.remove-message-pipes {
ul {
margin: 10px 0 0 12px;
padding: 0;
list-style: none;
border-left: 2px solid $border-color;
display: inline-block;
}
.mr-info-list {
position: relative;
margin: 10px 0 $gl-padding 12px;
li {
p {
margin: 6px 0;
position: relative;
margin: 0;
padding: 0;
display: block;
padding-left: 15px;
&::before {
content: '';
position: absolute;
border-top: 2px solid $border-color;
height: 1px;
top: 8px;
width: 8px;
left: 0;
}
&:last-child {
margin-bottom: 0;
span {
margin-left: 15px;
max-height: 20px;
&::before {
top: 14px;
}
}
}
li::before {
content: '';
.legend {
height: 100%;
width: 2px;
background: $border-color;
position: absolute;
border-top: 2px solid $border-color;
height: 1px;
top: 8px;
width: 8px;
top: -5px;
}
}
.mr-info-list.mr-memory-usage {
.legend {
height: 75%;
}
li:last-child {
p {
float: left;
padding-left: 20px;
&::before {
top: 18px;
top: 13px;
}
}
span {
display: block;
position: relative;
top: 5px;
margin-top: 5px;
}
.memory-graph-container {
float: left;
margin-left: 5px;
}
}
.mr-source-target {
background-color: $gray-light;
line-height: 31px;
border-style: solid;
border-width: 1px;
border-color: $border-color;
border-top-right-radius: 3px;
border-top-left-radius: 3px;
border-bottom: none;
padding: 16px;
margin-bottom: -1px;
border-radius: 3px 3px 0 0;
border-bottom: 1px solid $border-color;
padding: 0 $gl-padding;
margin-bottom: 6px;
line-height: 44px;
.dropdown-toggle .fa {
color: $gl-text-color;
}
}
.panel-new-merge-request {
......@@ -587,3 +729,20 @@
}
}
}
.mr-memory-usage {
p.usage-info-loading {
margin-bottom: 6px;
.usage-info-load-spinner {
margin-right: 10px;
font-size: 16px;
}
}
@media (max-width: $screen-md-min) {
.mr-info-list.mr-memory-usage .legend {
height: 80%;
}
}
}
......@@ -100,7 +100,10 @@ class ApplicationController < ActionController::Base
end
def access_denied!
render "errors/access_denied", layout: "errors", status: 404
respond_to do |format|
format.json { head :not_found }
format.any { render "errors/access_denied", layout: "errors", status: 404 }
end
end
def git_not_found!
......
......@@ -84,6 +84,7 @@ class Projects::BranchesController < Projects::ApplicationController
end
format.js { render nothing: true, status: result[:return_code] }
format.json { render json: { message: result[:message] }, status: result[:return_code] }
end
end
......
......@@ -91,7 +91,7 @@ class Projects::BuildsController < Projects::ApplicationController
def status
render json: BuildSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent_status(@build)
end
......
......@@ -39,7 +39,7 @@ class Projects::CommitController < Projects::ApplicationController
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent(@pipelines)
end
end
......
......@@ -10,8 +10,22 @@ class Projects::DeploymentsController < Projects::ApplicationController
.represent_concise(deployments) }
end
def metrics
@metrics = deployment.metrics(1.hour)
if @metrics&.any?
render json: @metrics, status: :ok
else
head :no_content
end
end
private
def deployment
@deployment ||= environment.deployments.find_by(iid: params[:id])
end
def environment
@environment ||= project.environments.find(params[:environment_id])
end
......
......@@ -17,7 +17,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.json do
render json: {
environments: EnvironmentSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.within_folders
.represent(@environments),
......@@ -37,7 +37,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
format.json do
render json: {
environments: EnvironmentSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@environments),
available_count: folder_environments.available.count,
......@@ -81,10 +81,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
stop_action = @environment.stop_with_action!(current_user)
if stop_action
redirect_to polymorphic_path([project.namespace.becomes(Namespace), project, stop_action])
else
redirect_to namespace_project_environment_path(project.namespace, project, @environment)
action_or_env_url =
if stop_action
polymorphic_url([project.namespace.becomes(Namespace), project, stop_action])
else
namespace_project_environment_url(project.namespace, project, @environment)
end
respond_to do |format|
format.html { redirect_to action_or_env_url }
format.json { render json: { redirect_url: action_or_env_url } }
end
end
......
......@@ -10,11 +10,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
:edit, :update, :show, :diffs, :commits, :conflicts, :conflict_for_path, :pipelines, :merge, :merge_check,
:ci_status, :pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues
:pipeline_status, :ci_environments_status, :toggle_subscription, :cancel_merge_when_pipeline_succeeds, :remove_wip, :resolve_conflicts, :assign_related_issues, :commit_change_content
]
before_action :validates_merge_request, only: [:show, :diffs, :commits, :pipelines]
before_action :define_show_vars, only: [:show, :diffs, :commits, :conflicts, :conflict_for_path, :builds, :pipelines]
before_action :define_widget_vars, only: [:merge, :cancel_merge_when_pipeline_succeeds, :merge_check]
before_action :define_commit_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :conflict_for_path, :pipelines]
before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
......@@ -74,10 +73,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def show
respond_to do |format|
format.html { define_discussion_vars }
format.html do
define_discussion_vars
end
format.json do
render json: MergeRequestSerializer.new.represent(@merge_request)
render json: serializer.represent(@merge_request, basic: params[:basic])
end
format.patch do
......@@ -214,7 +215,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent(@pipelines)
end
end
......@@ -230,7 +231,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: {
pipelines: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent(@pipelines)
}
end
......@@ -299,17 +300,21 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def remove_wip
MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
@merge_request = MergeRequests::UpdateService
.new(project, current_user, wip_event: 'unwip')
.execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged."
render json: serializer.represent(@merge_request)
end
def merge_check
@merge_request.check_if_can_be_merged
@pipelines = @merge_request.all_pipelines
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
render json: serializer.represent(@merge_request)
end
def commit_change_content
render partial: 'projects/merge_requests/widget/commit_change_content', layout: false
end
def cancel_merge_when_pipeline_succeeds
......@@ -320,65 +325,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user)
.cancel(@merge_request)
render json: serializer.represent(@merge_request)
end
def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
# Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
# to wait until CI completes to know
unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
@status = :failed
return
end
if params[:sha] != @merge_request.diff_head_sha
@status = :sha_mismatch
return
end
@merge_request.update(merge_error: nil)
status = merge!
if params[:merge_when_pipeline_succeeds].present?
unless @merge_request.head_pipeline
@status = :failed
return
end
if @merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params)
.execute(@merge_request)
@status = :merge_when_pipeline_succeeds
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
else
@status = :failed
end
if @merge_request.merge_error
render json: { status: status, merge_error: @merge_request.merge_error }
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = :success
render json: { status: status }
end
end
def merge_widget_refresh
@status =
if merge_request.merge_when_pipeline_succeeds
:merge_when_pipeline_succeeds
else
# Only MRs that can be merged end in this action
# MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
# in last case it does not have any special status. Possible error is handled inside widget js function
:success
end
render 'merge'
end
def branch_from
# This is always source
@source_project = @merge_request.nil? ? @project : @merge_request.source_project
......@@ -428,37 +390,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
def ci_status
pipeline = @merge_request.head_pipeline
@pipelines = @merge_request.all_pipelines
if pipeline
status = pipeline.status
coverage = pipeline.coverage
status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
status ||= "preparing"
else
ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
end
response = {
title: merge_request.title,
sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status,
coverage: coverage,
pipeline: pipeline.try(:id),
has_ci: @merge_request.has_ci?
}
render json: response
end
def pipeline_status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent_status(@merge_request.head_pipeline)
end
......@@ -474,10 +408,19 @@ class Projects::MergeRequestsController < Projects::ApplicationController
stop_namespace_project_environment_path(project.namespace, project, environment)
end
metrics_url =
if can?(current_user, :read_environment, environment) && environment.has_metrics?
metrics_namespace_project_environment_path(environment.project.namespace,
environment.project,
environment,
deployment)
end
{
id: environment.id,
name: environment.name,
url: namespace_project_environment_path(project.namespace, project, environment),
metrics_url: metrics_url,
stop_url: stop_url,
external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url,
......@@ -555,10 +498,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
end
def define_widget_vars
@pipeline = @merge_request.head_pipeline
end
def define_commit_vars
@commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit || @merge_request.likely_diff_base_commit
......@@ -694,4 +633,46 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.close
end
end
private
def merge!
# Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
# to wait until CI completes to know
unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
return :failed
end
return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
@merge_request.update(merge_error: nil)
if params[:merge_when_pipeline_succeeds].present?
return :failed unless @merge_request.head_pipeline
if @merge_request.head_pipeline.active?
MergeRequests::MergeWhenPipelineSucceedsService
.new(@project, current_user, merge_params)
.execute(@merge_request)
:merge_when_pipeline_succeeds
elsif @merge_request.head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
:success
else
:failed
end
else
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
:success
end
end
def serializer
MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
end
end
......@@ -37,7 +37,7 @@ class Projects::PipelinesController < Projects::ApplicationController
render json: {
pipelines: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines),
count: {
......@@ -74,7 +74,7 @@ class Projects::PipelinesController < Projects::ApplicationController
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent(@pipeline, grouped: true)
end
end
......@@ -94,7 +94,7 @@ class Projects::PipelinesController < Projects::ApplicationController
def status
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.new(project: @project, current_user: @current_user)
.represent_status(@pipeline)
end
......
......@@ -37,7 +37,10 @@ module IssuablesHelper
when Issue
IssueSerializer.new.represent(issuable).to_json
when MergeRequest
MergeRequestSerializer.new.represent(issuable).to_json
MergeRequestSerializer
.new(current_user: current_user, project: issuable.project)
.represent(issuable)
.to_json
end
end
......
......@@ -19,14 +19,6 @@ module MergeRequestsHelper
}
end
def mr_widget_refresh_url(mr)
if mr && mr.target_project
merge_widget_refresh_namespace_project_merge_request_url(mr.target_project.namespace, mr.target_project, mr)
else
''
end
end
def mr_css_classes(mr)
classes = "merge-request"
classes << " closed" if mr.closed?
......@@ -55,23 +47,6 @@ module MergeRequestsHelper
end
end
def issues_sentence(issues)
# Issuable sorter will sort local issues, then issues from the same
# namespace, then all other issues.
issues = Gitlab::IssuableSorter.sort(@project, issues).map do |issue|
issue.to_reference(@project)
end
issues.to_sentence
end
def mr_closes_issues
@mr_closes_issues ||= @merge_request.closes_issues(current_user)
end
def mr_issues_mentioned_but_not_closing
@mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
end
def mr_change_branches_path(merge_request)
new_namespace_project_merge_request_path(
@project.namespace, @project,
......@@ -85,35 +60,6 @@ module MergeRequestsHelper
)
end
def mr_assign_issues_link
issues = MergeRequests::AssignIssuesService.new(@project,
current_user,
merge_request: @merge_request,
closes_issues: mr_closes_issues
).assignable_issues
path = assign_related_issues_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
if issues.present?
pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
end
end
def source_branch_with_namespace(merge_request)
namespace = merge_request.source_project_namespace
branch = merge_request.source_branch
if merge_request.source_branch_exists?
namespace = link_to(namespace, project_path(merge_request.source_project))
branch = link_to(branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
end
if merge_request.for_fork?
namespace + ":" + branch
else
branch
end
end
def format_mr_branch_names(merge_request)
source_path = merge_request.source_project_path
target_path = merge_request.target_project_path
......
......@@ -92,7 +92,7 @@ module Mentionable
# Uses regex to quickly determine if mentionables might be referenced
# Allows heavy processing to be skipped
def matches_cross_reference_regex?
reference_pattern = if project.default_issues_tracker?
reference_pattern = if !project || project.default_issues_tracker?
ReferenceRegexes::DEFAULT_PATTERN
else
ReferenceRegexes::EXTERNAL_PATTERN
......
......@@ -99,6 +99,21 @@ class Deployment < ActiveRecord::Base
created_at.to_time.in_time_zone.to_s(:medium)
end
def has_metrics?
project.monitoring_service.present?
end
def metrics(timeframe)
return {} unless has_metrics?
half_timeframe = timeframe / 2
timeframe_start = created_at - half_timeframe
timeframe_end = created_at + half_timeframe
metrics = project.monitoring_service.metrics(environment, timeframe_start: timeframe_start, timeframe_end: timeframe_end)
metrics&.merge(deployment_time: created_at.to_i) || {}
end
private
def ref_path
......
......@@ -864,7 +864,7 @@ class MergeRequest < ActiveRecord::Base
end
def can_be_cherry_picked?
merge_commit
merge_commit.present?
end
def has_complete_diff_refs?
......@@ -908,6 +908,8 @@ class MergeRequest < ActiveRecord::Base
end
def conflicts_can_be_resolved_by?(user)
return false unless source_project
access = ::Gitlab::UserAccess.new(user, project: source_project)
access.can_push_to_branch?(source_branch)
end
......
......@@ -150,7 +150,7 @@ class ChatNotificationService < Service
def notify_for_ref?(data)
return true if data[:object_attributes][:tag]
return true unless notify_only_default_branch
return true unless notify_only_default_branch?
data[:object_attributes][:ref] == project.default_branch
end
......
......@@ -10,7 +10,7 @@ class MonitoringService < Service
end
# Environments have a number of metrics
def metrics(environment)
def metrics(environment, timeframe_start: nil, timeframe_end: nil)
raise NotImplementedError
end
end
class PrometheusService < MonitoringService
include ReactiveCaching
include ReactiveService
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
self.reactive_cache_lease_timeout = 30.seconds
self.reactive_cache_refresh_interval = 30.seconds
self.reactive_cache_lifetime = 1.minute
......@@ -64,16 +63,22 @@ class PrometheusService < MonitoringService
{ success: false, result: err }
end
def metrics(environment)
with_reactive_cache(environment.slug) do |data|
def metrics(environment, timeframe_start: nil, timeframe_end: nil)
with_reactive_cache(environment.slug, timeframe_start, timeframe_end) do |data|
data
end
end
# Cache metrics for specific environment
def calculate_reactive_cache(environment_slug)
def calculate_reactive_cache(environment_slug, timeframe_start, timeframe_end)
return unless active? && project && !project.pending_delete?
timeframe_start = Time.parse(timeframe_start) if timeframe_start
timeframe_end = Time.parse(timeframe_end) if timeframe_end
timeframe_start ||= 8.hours.ago
timeframe_end ||= Time.now
memory_query = %{(sum(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"}) / count(container_memory_usage_bytes{container_name!="POD",environment="#{environment_slug}"})) /1024/1024}
cpu_query = %{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
......@@ -81,11 +86,13 @@ class PrometheusService < MonitoringService
success: true,
metrics: {
# Average Memory used in MB
memory_values: client.query_range(memory_query, start: 8.hours.ago),
memory_current: client.query(memory_query),
memory_values: client.query_range(memory_query, start: timeframe_start, stop: timeframe_end),
memory_current: client.query(memory_query, time: timeframe_end),
memory_previous: client.query(memory_query, time: timeframe_start),
# Average CPU Utilization
cpu_values: client.query_range(cpu_query, start: 8.hours.ago),
cpu_current: client.query(cpu_query)
cpu_values: client.query_range(cpu_query, start: timeframe_start, stop: timeframe_end),
cpu_current: client.query(cpu_query, time: timeframe_end),
cpu_previous: client.query(cpu_query, time: timeframe_start)
},
last_update: Time.now.utc
}
......
class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
include GitlabRoutingHelper
include MarkupHelper
include TreeHelper
presents :merge_request
def ci_status
if pipeline
status = pipeline.status
status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
status || "preparing"
else
ci_service = source_project.try(:ci_service)
ci_service&.commit_status(diff_head_sha, source_branch)
end
end
def cancel_merge_when_pipeline_succeeds_path
if can_cancel_merge_when_pipeline_succeeds?(current_user)
cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(
project.namespace,
project,
merge_request)
end
end
def create_issue_to_resolve_discussions_path
if can?(current_user, :create_issue, project) && project.issues_enabled?
new_namespace_project_issue_path(project.namespace,
project,
merge_request_to_resolve_discussions_of: iid)
end
end
def remove_wip_path
if can?(current_user, :update_merge_request, merge_request.project)
remove_wip_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
def merge_path
if can_be_merged_by?(current_user)
merge_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
def revert_in_fork_path
if user_can_fork_project? && can_be_reverted?(current_user)
continue_params = {
to: merge_request_path(merge_request),
notice: "#{edit_in_new_fork_notice} Try to cherry-pick this commit again.",
notice_now: edit_in_new_fork_notice_now
}
namespace_project_forks_path(merge_request.project.namespace, merge_request.project,
namespace_key: current_user.namespace.id,
continue: continue_params)
end
end
def cherry_pick_in_fork_path
if user_can_fork_project? && can_be_cherry_picked?
continue_params = {
to: merge_request_path(merge_request),
notice: "#{edit_in_new_fork_notice} Try to revert this commit again.",
notice_now: edit_in_new_fork_notice_now
}
namespace_project_forks_path(project.namespace, project,
namespace_key: current_user.namespace.id,
continue: continue_params)
end
end
def conflict_resolution_path
if conflicts_can_be_resolved_in_ui? && conflicts_can_be_resolved_by?(current_user)
conflicts_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
end
def target_branch_commits_path
if target_branch_exists?
namespace_project_commits_path(project.namespace, project, target_branch)
end
end
def source_branch_path
if source_branch_exists?
namespace_project_branch_path(source_project.namespace, source_project, source_branch)
end
end
def source_branch_with_namespace_link
namespace = source_project_namespace
branch = source_branch
if source_branch_exists?
namespace = link_to(namespace, project_path(source_project))
branch = link_to(branch, namespace_project_commits_path(source_project.namespace, source_project, source_branch))
end
if for_fork?
namespace + ":" + branch
else
branch
end
end
def closing_issues_links
markdown issues_sentence(project, closing_issues), pipeline: :gfm, author: author, project: project
end
def mentioned_issues_links
mentioned_issues = issues_mentioned_but_not_closing(current_user)
markdown issues_sentence(project, mentioned_issues), pipeline: :gfm, author: author, project: project
end
def assign_to_closing_issues_link
issues = MergeRequests::AssignIssuesService.new(project,
current_user,
merge_request: merge_request,
closes_issues: closing_issues
).assignable_issues
path = assign_related_issues_namespace_project_merge_request_path(project.namespace, project, merge_request)
if issues.present?
pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue"
link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post
end
end
def can_revert_on_current_merge_request?
user_can_collaborate_with_project? && can_be_reverted?(current_user)
end
def can_cherry_pick_on_current_merge_request?
user_can_collaborate_with_project? && can_be_cherry_picked?
end
private
def closing_issues
@closing_issues ||= closes_issues(current_user)
end
def pipeline
@pipeline ||= head_pipeline
end
def issues_sentence(project, issues)
# Sorting based on the `#123` or `group/project#123` reference will sort
# local issues first.
issues.map do |issue|
issue.to_reference(project)
end.sort.to_sentence
end
def user_can_collaborate_with_project?
can?(current_user, :push_code, project) ||
(current_user && current_user.already_forked?(project))
end
def user_can_fork_project?
can?(current_user, :fork_project, project)
end
end
......@@ -3,8 +3,10 @@ class BaseSerializer
@request = EntityRequest.new(parameters)
end
def represent(resource, opts = {})
self.class.entity_class
def represent(resource, opts = {}, entity_class = nil)
entity_class = entity_class || self.class.entity_class
entity_class
.represent(resource, opts.merge(request: @request))
.as_json
end
......
......@@ -19,6 +19,6 @@ class BuildActionEntity < Grape::Entity
alias_method :build, :object
def playable?
build.playable? && can?(request.user, :update_build, build)
build.playable? && can?(request.current_user, :update_build, build)
end
end
......@@ -26,11 +26,11 @@ class BuildEntity < Grape::Entity
alias_method :build, :object
def playable?
build.playable? && can?(request.user, :update_build, build)
build.playable? && can?(request.current_user, :update_build, build)
end
def detailed_status
build.detailed_status(request.user)
build.detailed_status(request.current_user)
end
def path_to(route, build)
......
......@@ -31,7 +31,7 @@ class EnvironmentEntity < Grape::Entity
end
expose :terminal_path, if: ->(environment, _) { environment.has_terminals? } do |environment|
can?(request.user, :admin_environment, environment.project) &&
can?(request.current_user, :admin_environment, environment.project) &&
terminal_namespace_project_environment_path(
environment.project.namespace,
environment.project,
......
class EventEntity < Grape::Entity
expose :author, using: UserEntity
expose :updated_at
end
......@@ -11,6 +11,6 @@ class JobGroupEntity < Grape::Entity
alias_method :group, :object
def detailed_status
group.detailed_status(request.user)
group.detailed_status(request.current_user)
end
end
class MergeRequestBasicEntity < Grape::Entity
expose :merge_status
expose :merge_error
expose :state
expose :source_branch_exists?, as: :source_branch_exists
expose :time_estimate
expose :total_time_spent
expose :human_time_estimate
expose :human_total_time_spent
end
class MergeRequestBasicSerializer < BaseSerializer
entity MergeRequestBasicEntity
end
class MergeRequestEntity < IssuableEntity
include RequestAwareEntity
expose :assignee_id
expose :in_progress_merge_commit_sha
expose :locked_at
......@@ -12,4 +14,174 @@ class MergeRequestEntity < IssuableEntity
expose :source_project_id
expose :target_branch
expose :target_project_id
# Events
expose :merge_event, using: EventEntity
expose :closed_event, using: EventEntity
# User entities
expose :author, using: UserEntity
expose :merge_user, using: UserEntity
# Diff sha's
expose :diff_head_sha do |merge_request|
merge_request.diff_head_sha if merge_request.diff_head_commit
end
expose :merge_commit_sha
expose :merge_commit_message
expose :head_pipeline, with: PipelineEntity, as: :pipeline
# Booleans
expose :work_in_progress?, as: :work_in_progress
expose :source_branch_exists?, as: :source_branch_exists
expose :mergeable_discussions_state?, as: :mergeable_discussions_state
expose :branch_missing?, as: :branch_missing
expose :commits_count
expose :cannot_be_merged?, as: :has_conflicts
expose :can_be_merged?, as: :can_be_merged
expose :project_archived do |merge_request|
merge_request.project.archived?
end
expose :only_allow_merge_if_pipeline_succeeds do |merge_request|
merge_request.project.only_allow_merge_if_pipeline_succeeds?
end
# CI related
expose :has_ci?, as: :has_ci
expose :ci_status do |merge_request|
presenter(merge_request).ci_status
end
expose :issues_links do
expose :assign_to_closing do |merge_request|
presenter(merge_request).assign_to_closing_issues_link
end
expose :closing do |merge_request|
presenter(merge_request).closing_issues_links
end
expose :mentioned_but_not_closing do |merge_request|
presenter(merge_request).mentioned_issues_links
end
end
expose :source_branch_with_namespace_link do |merge_request|
presenter(merge_request).source_branch_with_namespace_link
end
expose :source_branch_path do |merge_request|
presenter(merge_request).source_branch_path
end
expose :current_user do
expose :can_remove_source_branch do |merge_request|
merge_request.source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
end
expose :can_revert_on_current_merge_request do |merge_request|
presenter(merge_request).can_revert_on_current_merge_request?
end
expose :can_cherry_pick_on_current_merge_request do |merge_request|
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
end
# Paths
#
expose :target_branch_commits_path do |merge_request|
presenter(merge_request).target_branch_commits_path
end
expose :conflict_resolution_path do |merge_request|
presenter(merge_request).conflict_resolution_path
end
expose :remove_wip_path do |merge_request|
presenter(merge_request).remove_wip_path
end
expose :cancel_merge_when_pipeline_succeeds_path do |merge_request|
presenter(merge_request).cancel_merge_when_pipeline_succeeds_path
end
expose :create_issue_to_resolve_discussions_path do |merge_request|
presenter(merge_request).create_issue_to_resolve_discussions_path
end
expose :merge_path do |merge_request|
presenter(merge_request).merge_path
end
expose :cherry_pick_in_fork_path do |merge_request|
presenter(merge_request).cherry_pick_in_fork_path
end
expose :revert_in_fork_path do |merge_request|
presenter(merge_request).revert_in_fork_path
end
expose :email_patches_path do |merge_request|
namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request,
format: :patch)
end
expose :plain_diff_path do |merge_request|
namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request,
format: :diff)
end
expose :status_path do |merge_request|
namespace_project_merge_request_path(merge_request.target_project.namespace,
merge_request.target_project,
merge_request,
format: :json)
end
expose :merge_check_path do |merge_request|
merge_check_namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request)
end
expose :ci_environments_status_path do |merge_request|
ci_environments_status_namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request)
end
expose :merge_commit_message_with_description do |merge_request|
merge_request.merge_commit_message(include_description: true)
end
expose :diverged_commits_count do |merge_request|
if merge_request.open? && merge_request.diverged_from_target_branch?
merge_request.diverged_commits_count
else
0
end
end
expose :commit_change_content_path do |merge_request|
commit_change_content_namespace_project_merge_request_path(merge_request.project.namespace,
merge_request.project,
merge_request)
end
private
delegate :current_user, to: :request
def presenter(merge_request)
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user)
end
end
class MergeRequestSerializer < BaseSerializer
entity MergeRequestEntity
# This overrided method takes care of which entity should be used
# to serialize the `merge_request` based on `basic` key in `opts` param.
# Hence, `entity` doesn't need to be declared on the class scope.
def represent(merge_request, opts = {})
entity = opts[:basic] ? MergeRequestBasicEntity : MergeRequestEntity
super(merge_request, opts, entity)
end
end
......@@ -3,6 +3,8 @@ class PipelineEntity < Grape::Entity
expose :id
expose :user, using: UserEntity
expose :active?, as: :active
expose :coverage
expose :path do |pipeline|
namespace_project_pipeline_path(
......@@ -69,16 +71,16 @@ class PipelineEntity < Grape::Entity
alias_method :pipeline, :object
def can_retry?
can?(request.user, :update_pipeline, pipeline) &&
can?(request.current_user, :update_pipeline, pipeline) &&
pipeline.retryable?
end
def can_cancel?
can?(request.user, :update_pipeline, pipeline) &&
can?(request.current_user, :update_pipeline, pipeline) &&
pipeline.cancelable?
end
def detailed_status
pipeline.detailed_status(request.user)
pipeline.detailed_status(request.current_user)
end
end
......@@ -37,4 +37,11 @@ class PipelineSerializer < BaseSerializer
data = represent(resource, { only: [{ details: [:status] }] })
data.dig(:details, :status) || {}
end
def represent_stages(resource)
return {} unless resource.present?
data = represent(resource, { only: [{ details: [:stages] }] })
data.dig(:details, :stages) || []
end
end
......@@ -35,6 +35,6 @@ class StageEntity < Grape::Entity
alias_method :stage, :object
def detailed_status
stage.detailed_status(request.user)
stage.detailed_status(request.current_user)
end
end
......@@ -50,7 +50,7 @@ module Ci
when 'always'
%w[success failed skipped]
when 'manual'
%w[success]
%w[success skipped]
else
[]
end
......
......@@ -35,7 +35,7 @@
= link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
Merge Requests
%span.badge.count.merge_counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments, :artifacts]) do
......
- @content_class = "limit-container-width" unless fluid_layout
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
......@@ -11,42 +11,17 @@
.merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
= render "projects/merge_requests/show/mr_box"
.append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
- if koding_enabled? && @repository.koding_yml
= link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do
Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
%span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
%span <b>Request to merge</b>
%span.label-branch= source_branch_with_namespace(@merge_request)
%span <b>into</b>
%span.label-branch
= link_to_if @merge_request.target_branch_exists?, @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch)
- if @merge_request.open? && @merge_request.diverged_from_target_branch?
%span (#{pluralize(@merge_request.diverged_commits_count, 'commit')} behind)
- if @merge_request.source_branch_exists?
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
:javascript
window.gl.mrWidgetData = #{serialize_issuable(@merge_request)}
#js-vue-mr-widget.mr-widget
- if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.merge-manually.light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('vue_merge_request_widget')
.content-block.content-block-small.emoji-list-container
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
......@@ -113,9 +88,7 @@
:javascript
$(function () {
new MergeRequest({
window.mergeRequest = new MergeRequest({
action: "#{controller.action_name}"
});
});
var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}";
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}");
- case @status
- when :success
- remove_source_branch = params[:should_remove_source_branch] == '1' || @merge_request.remove_source_branch?
:plain
merge_request_widget.mergeInProgress(#{remove_source_branch});
- when :merge_when_pipeline_succeeds
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
- when :sha_mismatch
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
- else
:plain
$('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
.mr-state-widget
= render 'projects/merge_requests/widget/heading'
.mr-widget-body
%h4
Closed
- if @merge_request.closed_event
by #{link_to_member(@project, @merge_request.closed_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.closed_event.created_at)}
%p
= succeed '.' do
The changes were not merged into
%span.label-branch= @merge_request.target_branch
- if @merge_request.can_be_reverted?(current_user)
= render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
- if @merge_request.can_be_cherry_picked?
= render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped manual canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
%div{ class: "ci-status-icon ci-status-icon-#{status}" }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
= ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
- if @pipeline.stages.any?
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
%span
for
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
-# Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
-# TODO, remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- %w[success skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" }
= ci_icon_for_status(status)
%span
CI job
= ci_label_for_status(status)
for
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
%span.ci-coverage
.ci_widget
= icon("spinner spin")
Checking CI status for #{@merge_request.diff_head_commit.short_id}&hellip;
.ci_widget.ci-not_found{ style: "display:none" }
= icon("times-circle")
Could not find CI status for #{@merge_request.diff_head_commit.short_id}.
.ci_widget.ci-error{ style: "display:none" }
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
.js-success-icon.hidden
= ci_icon_for_status('success')
.mr-state-widget
= render 'projects/merge_requests/widget/heading'
.mr-widget-body
%h4
= icon("spinner spin")
Merge in progress&hellip;
%p
This merge request is in the process of being merged, during which time it is locked and cannot be closed.
.mr-state-widget
= render 'projects/merge_requests/widget/heading'
.mr-widget-body
%h4
Merged
- if @merge_request.merge_event
by #{link_to_member(@project, @merge_request.merge_event.author, avatar: true)}
#{time_ago_with_tooltip(@merge_request.merge_event.created_at)}
- if !@merge_request.source_branch_exists? || params[:deleted_source_branch]
.remove-message-pipes
%ul
%li
%span
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
%li
%span
The source branch has been removed.
= render 'projects/merge_requests/widget/merged_buttons'
- elsif @merge_request.can_remove_source_branch?(current_user)
.remove_source_branch_widget.remove-message-pipes
%ul
%li
%span
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
%li
%span
You can remove the source branch now.
= render 'projects/merge_requests/widget/merged_buttons', source_branch_exists: true
.remove_source_branch_widget.failed.remove-message-pipes.hide
%ul
%li
%span
Failed to remove source branch '#{@merge_request.source_branch}'.
.remove_source_branch_in_progress.remove-message-pipes.hide
%ul
%li
%span
= icon('spinner spin')
Removing source branch '#{@merge_request.source_branch}'.
%li
%span
Please wait, this page will be automatically reloaded.
- else
.remove-message-pipes
%ul
%li
%span
The changes were merged into
#{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
= render 'projects/merge_requests/widget/merged_buttons'
- can_remove_source_branch = local_assigns.fetch(:source_branch_exists, false) && @merge_request.can_remove_source_branch?(current_user)
- mr_can_be_reverted = @merge_request.can_be_reverted?(current_user)
- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked?
- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.clearfix.merged-buttons
- if can_remove_source_branch
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default remove_source_branch" do
= icon('trash-o')
Remove source branch
- if mr_can_be_reverted
= revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "close")
- if mr_can_be_cherry_picked
= cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: "default")
.mr-state-widget
= render 'projects/merge_requests/widget/heading'
.mr-widget-body
-# After conflicts are resolved, the user is redirected back to the MR page.
-# There is a short window before background workers run and GitLab processes
-# the new push and commits, during which it will think the conflicts still exist.
-# We send this param to get the widget to treat the MR as having no more conflicts.
- resolved_conflicts = params[:resolved_conflicts]
- if @project.archived?
= render 'projects/merge_requests/widget/open/archived'
- elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch'
- elsif @merge_request.has_no_commits?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check'
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
- elsif @merge_request.merge_when_pipeline_succeeds? && @merge_request.merge_error.present?
= render 'projects/merge_requests/widget/open/error'
- elsif @merge_request.merge_when_pipeline_succeeds?
= render 'projects/merge_requests/widget/open/merge_when_pipeline_succeeds'
- elsif !@merge_request.can_be_merged_by?(current_user)
= render 'projects/merge_requests/widget/open/not_allowed'
- elsif !@merge_request.mergeable_ci_state? && (@pipeline.failed? || @pipeline.canceled?)
= render 'projects/merge_requests/widget/open/build_failed'
- elsif !@merge_request.mergeable_discussions_state?
= render 'projects/merge_requests/widget/open/unresolved_discussions'
- elsif @pipeline&.blocked?
= render 'projects/merge_requests/widget/open/manual'
- elsif @merge_request.can_be_merged? || resolved_conflicts
= render 'projects/merge_requests/widget/open/accept'
- if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present?
.mr-widget-footer
%span
= icon('check')
- if mr_closes_issues.present?
Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
= succeed '.' do
!= markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
= mr_assign_issues_link
- if mr_issues_mentioned_but_not_closing.present?
#{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
!= markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
#{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not be closed.
- if @merge_request.open?
= render 'projects/merge_requests/widget/open'
- elsif @merge_request.merged?
= render 'projects/merge_requests/widget/merged'
- elsif @merge_request.closed?
= render 'projects/merge_requests/widget/closed'
- elsif @merge_request.locked?
= render 'projects/merge_requests/widget/locked'
:javascript
var opts = {
merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
ci_status_url: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
pipeline_status_url: "#{pipeline_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_environments_status_url: "#{ci_environments_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
gitlab_icon: "#{asset_path 'gitlab_logo.png'}",
ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}",
ci_message: {
normal: "Pipeline {{status}} for \"{{title}}\"",
preparing: "{{status}} pipeline for \"{{title}}\""
},
ci_enable: #{@project.ci_service ? "true" : "false"},
ci_title: {
preparing: "{{status}} pipeline",
normal: "Pipeline {{status}}"
},
ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}",
ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json},
commits_path: "#{project_commits_path(@project)}",
pipeline_path: "#{project_pipelines_path(@project)}",
pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}"
};
if (typeof merge_request_widget !== 'undefined') {
merge_request_widget.cancelPolling();
merge_request_widget.clearEventListeners();
}
merge_request_widget = new window.gl.MergeRequestWidget(opts);
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('merge_request_widget')
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-quick-submit js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
= hidden_field_tag :sha, @merge_request.diff_head_sha
.accept-merge-holder.clearfix.js-toggle-container
.clearfix
.accept-action
- if @pipeline && @pipeline.active?
%span.btn-group
= button_tag class: "btn btn-info js-merge-when-pipeline-succeeds-button merge-when-pipeline-succeeds" do
Merge when pipeline succeeds
- unless @project.only_allow_merge_if_pipeline_succeeds?
= button_tag class: "btn btn-info dropdown-toggle", 'data-toggle' => 'dropdown' do
= icon('caret-down')
%span.sr-only
Select merge moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
%li
= link_to "#", class: "merge-when-pipeline-succeeds" do
= icon('check fw')
Merge when pipeline succeeds
%li
= link_to "#", class: "accept-merge-request" do
= icon('warning fw')
Merge immediately
- else
= f.button class: "btn btn-grouped js-merge-button accept-merge-request" do
Accept merge request
- if @merge_request.force_remove_source_branch?
.accept-control
The source branch will be removed.
- elsif @merge_request.can_remove_source_branch?(current_user)
.accept-control.checkbox
= label_tag :should_remove_source_branch, class: "merge-param-checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
.accept-control
%button.modify-merge-commit-link.js-toggle-button{ type: "button" }
= icon('edit')
Modify commit message
.js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
message_with_description: @merge_request.merge_commit_message(include_description: true),
message_without_description: @merge_request.merge_commit_message,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
= hidden_field_tag :merge_when_pipeline_succeeds, "", autocomplete: "off"
%h4
Project is archived
%p
This merge request cannot be merged because archived projects cannot be written to.
%h4
= icon('exclamation-triangle')
The pipeline for this merge request failed
%p
Please retry the job or push a new commit to fix the failure.
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('merge_request_widget')
%strong
= icon("spinner spin")
Checking ability to merge automatically&hellip;
- can_resolve = @merge_request.conflicts_can_be_resolved_by?(current_user)
- can_resolve_in_ui = @merge_request.conflicts_can_be_resolved_in_ui?
- can_merge = @merge_request.can_be_merged_via_command_line_by?(current_user)
%h4.has-conflicts
%p
= icon("exclamation-triangle")
This merge request contains merge conflicts
.remove-message-pipes
%ul
%li
%span
To merge this request, resolve these conflicts
- if can_resolve && !can_resolve_in_ui
locally
or
- unless can_merge
ask someone with write access to this repository to
merge it locally.
- if (can_resolve && can_resolve_in_ui) || can_merge
.merged-buttons.clearfix
- if can_resolve && can_resolve_in_ui
= link_to "Resolve conflicts", conflicts_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn"
- if can_merge
= link_to "Merge locally", "#modal_merge_info", class: "btn how_to_merge_link vlink", "data-toggle" => "modal"
%h4
Pipeline blocked
%p
The pipeline for this merge request requires a manual action to proceed.
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('merge_request_widget')
%h4
Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)}
to be merged automatically when the pipeline succeeds.
.remove-message-pipes
%ul
%li
%span
= succeed '.' do
The changes will be merged into #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}
- if @merge_request.remove_source_branch?
%li
%span
The source branch will be removed.
- else
%li
%span
The source branch will not be removed.
- remove_source_branch_button = !@merge_request.remove_source_branch? && @merge_request.can_remove_source_branch?(current_user) && @merge_request.merge_user == current_user
- user_can_cancel_automatic_merge = @merge_request.can_cancel_merge_when_pipeline_succeeds?(current_user)
- if remove_source_branch_button || user_can_cancel_automatic_merge
.clearfix.prepend-top-10
- if remove_source_branch_button
= link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do
= icon('times')
Remove source branch when merged
- if user_can_cancel_automatic_merge
= link_to cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-sm" do
Cancel automatic merge
- unless @merge_request.source_branch_exists?
%h4
= icon("exclamation-triangle")
Source branch
%span.label-branch= source_branch_with_namespace(@merge_request)
does not exist
%p
Please restore the source branch or close this merge request and open a new merge request with a different source branch.
- else
%h4
= icon("exclamation-triangle")
Target branch
%span.label-branch= @merge_request.target_branch
does not exist
%p
Please restore the target branch or use a different target branch.
%h4
Ready to be merged automatically
%p
Ask someone with write access to this repository to merge this request.
- if @merge_request.force_remove_source_branch?
The source branch will be removed.
%h4
= icon("exclamation-triangle")
Nothing to merge from
%span.label-branch= source_branch_with_namespace(@merge_request)
into
%span.label-branch= @merge_request.target_branch
%p
Please push new commits to the source branch or use a different target branch.
%h4
= icon("exclamation-triangle")
This merge request failed to be merged automatically
%p
Please reload the page to find out the reason.
%h4
= icon("exclamation-triangle")
This merge request has received new commits since the page was loaded.
%p
Please reload the page to review the new commits before merging.
%h4
= icon('exclamation-triangle')
This merge request has unresolved discussions
%p
Please resolve these discussions
- if @project.issues_enabled? && can?(current_user, :create_issue, @project)
or
= link_to "open an issue to resolve them later", new_namespace_project_issue_path(@project.namespace, @project, merge_request_to_resolve_discussions_of: @merge_request.iid)
to allow this merge request to be merged.
%h4
This merge request is currently a Work In Progress
- if can?(current_user, :update_merge_request, @merge_request)
%p
When this merge request is ready,
= link_to remove_wip_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), method: :post do
remove the
%code WIP:
prefix from the title
to allow it to be merged.
......@@ -5,12 +5,3 @@
-# This check is duplicated below, to avoid conflicts with EE.
- return unless issuable.can_remove_source_branch?(current_user)
.form-group
.col-sm-10.col-sm-offset-2
- if issuable.can_remove_source_branch?(current_user)
.checkbox
= label_tag 'merge_request[force_remove_source_branch]' do
= hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
= check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
Remove source branch when merge request is accepted.
---
title: Fix skipped manual actions problem when processing the pipeline
merge_request: 11164
author:
---
title: Fix notify_only_default_branch check for Slack service
merge_request:
author:
---
title: Fix up arrow not editing last discussion comment
merge_request:
author:
......@@ -75,10 +75,9 @@ constraints(ProjectUrlConstrainer.new) do
get :conflict_for_path
get :pipelines
get :merge_check
get :commit_change_content
post :merge
get :merge_widget_refresh
post :cancel_merge_when_pipeline_succeeds
get :ci_status
get :pipeline_status
get :ci_environments_status
post :toggle_subscription
......@@ -146,7 +145,11 @@ constraints(ProjectUrlConstrainer.new) do
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end
resources :deployments, only: [:index]
resources :deployments, only: [:index] do
member do
get :metrics
end
end
end
resource :cycle_analytics, only: [:show]
......
......@@ -42,7 +42,6 @@ var config = {
locale: './locale/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
merge_request_widget: './merge_request_widget/ci_bundle.js',
monitoring: './monitoring/monitoring_bundle.js',
network: './network/network_bundle.js',
notebook_viewer: './blob/notebook_viewer.js',
......@@ -63,6 +62,7 @@ var config = {
u2f: ['vendor/u2f'],
users: './users/users_bundle.js',
raven: './raven/index.js',
vue_merge_request_widget: './vue_merge_request_widget/index.js',
},
output: {
......
......@@ -169,6 +169,14 @@ Clicking on the Monitoring button will display a new page, showing up to the las
8 hours of performance data. It may take a minute or two for data to appear
after initial deployment.
## Determining performance impact of a merge
> [Introduced][ce-10408] in GitLab 9.1.
After a merge request has been approved, a sparkline will appear on the merge request page displaying the average memory usage of the application. The sparkline includes thirty minutes of data prior to the merge, a dot to indicate the merge itself, and then will begin capturing thirty minutes of data after the merge.
This sparkline serves as a quick indicator of the impact on memory consumption of the recently merged changes. If there is a problem, action can then be taken to troubleshoot or revert the merge.
## Troubleshooting
If the "Attempting to load performance data" screen continues to appear, it could be due to:
......@@ -189,4 +197,5 @@ If the "Attempting to load performance data" screen continues to appear, it coul
[gitlab.com-ip-range]: https://gitlab.com/gitlab-com/infrastructure/issues/434
[ci-environment-slug]: https://docs.gitlab.com/ce/ci/variables/#predefined-variables-environment-variables
[ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935
[ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408
[promgldocs]: ../../../administration/monitoring/prometheus/index.md
......@@ -5,12 +5,14 @@ Feature: Revert Commits
And I own a project
And I visit my project's commits page
@javascript
Scenario: I revert a commit
Given I click on commit link
And I click on the revert button
And I revert the changes directly
Then I should see the revert commit notice
@javascript
Scenario: I revert a commit that was previously reverted
Given I click on commit link
And I click on the revert button
......@@ -21,6 +23,7 @@ Feature: Revert Commits
And I revert the changes directly
Then I should see a revert error
@javascript
Scenario: I revert a commit in a new merge request
Given I click on commit link
And I click on the revert button
......
......@@ -26,11 +26,13 @@ Feature: Project Merge Requests
When I visit project "Shop" merge requests page
Then I should see "feature_conflict" branch
@javascript
Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
Given project "Shop" have "Bug NS-07" open merge request with rebased branch
When I visit merge request page "Bug NS-07"
Then I should not see the diverged commits count
@javascript
Scenario: I should see the numbers of diverged commits if the branch diverged from the target
Given project "Shop" have "Bug NS-08" open merge request with diverged branch
When I visit merge request page "Bug NS-08"
......@@ -46,21 +48,25 @@ Feature: Project Merge Requests
Then I should see "Feature NS-03" in merge requests
And I should see "Bug NS-04" in merge requests
@javascript
Scenario: I visit an open merge request page
Given I click link "Bug NS-04"
Then I should see merge request "Bug NS-04"
@javascript
Scenario: I visit a merged merge request page
Given project "Shop" have "Feature NS-05" merged merge request
And I click link "Merged"
And I click link "Feature NS-05"
Then I should see merge request "Feature NS-05"
@javascript
Scenario: I close merge request page
Given I click link "Bug NS-04"
And I click link "Close"
Then I should see closed merge request "Bug NS-04"
@javascript
Scenario: I reopen merge request page
Given I click link "Bug NS-04"
And I click link "Close"
......@@ -176,6 +182,7 @@ Feature: Project Merge Requests
# Markdown
@javascript
Scenario: Headers inside the description should have ids generated for them.
When I visit merge request page "Bug NS-04"
Then Header "Description header" should have correct id and link
......
......@@ -7,7 +7,6 @@ Feature: Project Merge Requests Acceptance
@javascript
Scenario: Accepting the Merge Request and removing the source branch
Given I am on the Merge Request detail page
When I click on "Remove source branch" option
And I click on Accept Merge Request
Then I should see merge request merged
And I should not see the Remove Source Branch button
......@@ -15,7 +14,6 @@ Feature: Project Merge Requests Acceptance
@javascript
Scenario: Accepting the Merge Request when URL has an anchor
Given I am on the Merge Request detail with note anchor page
When I click on "Remove source branch" option
And I click on Accept Merge Request
Then I should see merge request merged
And I should not see the Remove Source Branch button
......@@ -23,6 +21,7 @@ Feature: Project Merge Requests Acceptance
@javascript
Scenario: Accepting the Merge Request without removing the source branch
Given I am on the Merge Request detail page
When I click on "Remove source branch" option
When I click on Accept Merge Request
Then I should see merge request merged
And I should see the Remove Source Branch button
......@@ -10,6 +10,7 @@ class Spinach::Features::RevertCommits < Spinach::FeatureSteps
end
step 'I click on the revert button' do
find(".header-action-buttons .dropdown").click
find("a[href='#modal-revert-commit']").click
end
......
......@@ -4,6 +4,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
include SharedNote
include SharedPaths
include Select2Helper
include WaitForVueResource
step 'I am a member of project "Shop"' do
@project = ::Project.find_by(name: "Shop")
......@@ -31,6 +32,8 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
expect(page).to have_content @project.path_with_namespace
expect(page).to have_content @merge_request.source_branch
expect(page).to have_content @merge_request.target_branch
wait_for_vue_resource
end
step 'I fill out a "Merge Request On Forked Project" merge request' do
......
......@@ -8,6 +8,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
include SharedDiffNote
include SharedUser
include WaitForAjax
include WaitForVueResource
after do
wait_for_ajax if javascript_test?
......@@ -45,19 +46,23 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within '.merge-request' do
expect(page).to have_content "Wiki Feature"
end
wait_for_vue_resource
end
step 'I should see closed merge request "Bug NS-04"' do
expect(page).to have_content "Bug NS-04"
expect(page).to have_content "Closed by"
wait_for_vue_resource
end
step 'I should see merge request "Bug NS-04"' do
expect(page).to have_content "Bug NS-04"
wait_for_vue_resource
end
step 'I should see merge request "Feature NS-05"' do
expect(page).to have_content "Feature NS-05"
wait_for_vue_resource
end
step 'I should not see "master" branch' do
......@@ -358,10 +363,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a badge of "1" next to the discussion link' do
expect_discussion_badge_to_have_counter("1")
wait_for_vue_resource
end
step 'I should see a badge of "0" next to the discussion link' do
expect_discussion_badge_to_have_counter("0")
wait_for_vue_resource
end
step 'I should see a discussion has started on commit diff' do
......@@ -369,6 +376,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content sample_commit.line_code_path
page.should have_content "Line is wrong"
wait_for_vue_resource
end
end
......@@ -376,16 +384,17 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} #{current_user.to_reference} started a discussion on commit"
page.should have_content "One comment to rule them all"
wait_for_vue_resource
end
end
step 'merge request is mergeable' do
expect(page).to have_button 'Accept merge request'
expect(page).to have_button 'Merge'
end
step 'I modify merge commit message' do
click_button "Modify commit message"
fill_in 'commit_message', with: 'wow such merge'
fill_in 'Commit message', with: 'wow such merge'
end
step 'merge request "Bug NS-05" is mergeable' do
......@@ -394,24 +403,26 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I accept this merge request' do
page.within '.mr-state-widget' do
click_button "Accept merge request"
click_button "Merge"
end
end
step 'I should see merged request' do
page.within '.status-box' do
expect(page).to have_content "Merged"
wait_for_vue_resource
end
end
step 'I click link "Reopen"' do
first(:css, '.reopen-mr-link').click
first(:css, '.reopen-mr-link').trigger('click')
end
step 'I should see reopened merge request "Bug NS-04"' do
page.within '.status-box' do
expect(page).to have_content "Open"
end
wait_for_vue_resource
end
step 'I click link "Hide inline discussion" of the third file' do
......@@ -435,6 +446,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see a comment like "Line is wrong" in the third file' do
page.within '.files>div:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong"
wait_for_vue_resource
end
end
......@@ -502,6 +514,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I should see comments on the side-by-side diff page' do
page.within '.files>div:nth-child(2) .parallel .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
wait_for_vue_resource
end
end
......@@ -557,12 +570,16 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.within ".mr-source-target" do
expect(page).to have_content /([0-9]+ commits behind)/
end
wait_for_vue_resource
end
step 'I should not see the diverged commits count' do
page.within ".mr-source-target" do
expect(page).not_to have_content /([0-9]+ commit[s]? behind)/
end
wait_for_vue_resource
end
def merge_request
......
class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
include LoginHelpers
include GitlabRoutingHelper
include WaitForAjax
include WaitForVueResource
step 'I am on the Merge Request detail page' do
visit merge_request_path(@merge_request)
......@@ -12,27 +12,27 @@ class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
end
step 'I click on "Remove source branch" option' do
check('Remove source branch')
uncheck('Remove source branch')
end
step 'I click on Accept Merge Request' do
click_button('Accept merge request')
click_button('Merge')
end
step 'I should see the Remove Source Branch button' do
expect(page).to have_link('Remove source branch')
expect(page).to have_selector('.js-remove-branch-button')
# Wait for AJAX requests to complete so they don't blow up if they are
# Wait for View Resource requests to complete so they don't blow up if they are
# only handled after `DatabaseCleaner` has already run
wait_for_ajax
wait_for_vue_resource
end
step 'I should not see the Remove Source Branch button' do
expect(page).not_to have_link('Remove source branch')
expect(page).not_to have_selector('.js-remove-branch-button')
# Wait for AJAX requests to complete so they don't blow up if they are
# Wait for View Resource requests to complete so they don't blow up if they are
# only handled after `DatabaseCleaner` has already run
wait_for_ajax
wait_for_vue_resource
end
step 'There is an open Merge Request' do
......
class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
include LoginHelpers
include GitlabRoutingHelper
include WaitForVueResource
step 'I click on the revert button' do
find("a[href='#modal-revert-commit']").click
......@@ -15,6 +16,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
step 'I should see the revert merge request notice' do
page.should have_content('The merge request has been successfully reverted.')
wait_for_vue_resource
end
step 'I should not see the revert button' do
......@@ -26,7 +28,7 @@ class Spinach::Features::RevertMergeRequests < Spinach::FeatureSteps
end
step 'I click on Accept Merge Request' do
click_button('Accept merge request')
click_button('Merge')
end
step 'I am signed in as a developer of the project' do
......
......@@ -2,6 +2,7 @@ module SharedPaths
include Spinach::DSL
include RepoHelpers
include DashboardHelper
include WaitForVueResource
step 'I visit new project page' do
visit new_project_path
......@@ -377,23 +378,28 @@ module SharedPaths
step 'I visit merge request page "Bug NS-04"' do
visit merge_request_path("Bug NS-04")
wait_for_vue_resource
end
step 'I visit merge request page "Bug NS-05"' do
visit merge_request_path("Bug NS-05")
wait_for_vue_resource
end
step 'I visit merge request page "Bug NS-07"' do
visit merge_request_path("Bug NS-07")
wait_for_vue_resource
end
step 'I visit merge request page "Bug NS-08"' do
visit merge_request_path("Bug NS-08")
wait_for_vue_resource
end
step 'I visit merge request page "Bug CO-01"' do
mr = MergeRequest.find_by(title: "Bug CO-01")
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
wait_for_vue_resource
end
step 'I visit project "Shop" merge requests page' do
......
......@@ -10,7 +10,7 @@ if ENV['CI']
Knapsack::Adapters::SpinachAdapter.bind
end
%w(select2_helper test_env repo_helpers wait_for_ajax wait_for_requests sidekiq).each do |f|
%w(select2_helper test_env repo_helpers wait_for_ajax wait_for_requests sidekiq wait_for_vue_resource).each do |f|
require Rails.root.join('spec', 'support', f)
end
......
......@@ -13,18 +13,18 @@ module Gitlab
json_api_get('query', query: '1')
end
def query(query)
def query(query, time: Time.now)
get_result('vector') do
json_api_get('query', query: query)
json_api_get('query', query: query, time: time.utc.to_f)
end
end
def query_range(query, start: 8.hours.ago)
def query_range(query, start: 8.hours.ago, stop: Time.now)
get_result('matrix') do
json_api_get('query_range',
query: query,
start: start.to_f,
end: Time.now.utc.to_f,
end: stop.to_f,
step: 1.minute.to_i)
end
end
......
......@@ -213,33 +213,98 @@ describe Projects::BranchesController do
sign_in(user)
post :destroy,
format: :js,
id: branch,
namespace_id: project.namespace,
project_id: project
format: format,
id: branch,
namespace_id: project.namespace,
project_id: project
end
context "valid branch name, valid source" do
context 'as JS' do
let(:branch) { "feature" }
let(:format) { :js }
it { expect(response).to have_http_status(200) }
end
context "valid branch name, valid source" do
let(:branch) { "feature" }
it { expect(response).to have_http_status(200) }
it { expect(response.body).to be_blank }
end
context "valid branch name with unencoded slashes" do
let(:branch) { "improve/awesome" }
it { expect(response).to have_http_status(200) }
it { expect(response.body).to be_blank }
end
context "valid branch name with encoded slashes" do
let(:branch) { "improve%2Fawesome" }
context "valid branch name with unencoded slashes" do
let(:branch) { "improve/awesome" }
it { expect(response).to have_http_status(200) }
it { expect(response.body).to be_blank }
end
it { expect(response).to have_http_status(200) }
context "invalid branch name, valid ref" do
let(:branch) { "no-branch" }
it { expect(response).to have_http_status(404) }
it { expect(response.body).to be_blank }
end
end
context "valid branch name with encoded slashes" do
let(:branch) { "improve%2Fawesome" }
context 'as JSON' do
let(:branch) { "feature" }
let(:format) { :json }
context 'valid branch name, valid source' do
let(:branch) { "feature" }
it { expect(response).to have_http_status(200) }
it 'returns JSON response with message' do
expect(json_response).to eql("message" => 'Branch was removed')
end
it { expect(response).to have_http_status(200) }
end
context 'valid branch name with unencoded slashes' do
let(:branch) { "improve/awesome" }
it 'returns JSON response with message' do
expect(json_response).to eql('message' => 'Branch was removed')
end
it { expect(response).to have_http_status(200) }
end
context "valid branch name with encoded slashes" do
let(:branch) { 'improve%2Fawesome' }
it 'returns JSON response with message' do
expect(json_response).to eql('message' => 'Branch was removed')
end
it { expect(response).to have_http_status(200) }
end
context 'invalid branch name, valid ref' do
let(:branch) { 'no-branch' }
it 'returns JSON response with message' do
expect(json_response).to eql('message' => 'No such branch')
end
it { expect(response).to have_http_status(404) }
end
end
context "invalid branch name, valid ref" do
let(:branch) { "no-branch" }
it { expect(response).to have_http_status(404) }
context 'as HTML' do
let(:branch) { "feature" }
let(:format) { :html }
it 'redirects to branches path' do
expect(response)
.to redirect_to(namespace_project_branches_path(project.namespace, project))
end
end
end
......
......@@ -8,7 +8,7 @@ describe Projects::DeploymentsController do
let(:environment) { create(:environment, name: 'production', project: project) }
before do
project.add_master(user)
project.team << [user, :master]
sign_in(user)
end
......@@ -19,7 +19,7 @@ describe Projects::DeploymentsController do
create(:deployment, environment: environment, created_at: 7.hours.ago)
create(:deployment, environment: environment)
get :index, environment_params(after: 8.hours.ago)
get :index, deployment_params(after: 8.hours.ago)
expect(response).to be_ok
......@@ -29,14 +29,59 @@ describe Projects::DeploymentsController do
it 'returns a list with deployments information' do
create(:deployment, environment: environment)
get :index, environment_params
get :index, deployment_params
expect(response).to be_ok
expect(response).to match_response_schema('deployments')
end
end
def environment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace, project_id: project, environment_id: environment.id)
describe 'GET #metrics' do
let(:deployment) { create(:deployment, project: project, environment: environment) }
before do
allow(controller).to receive(:deployment).and_return(deployment)
end
context 'when environment has no metrics' do
before do
expect(deployment).to receive(:metrics).and_return(nil)
end
it 'returns a empty response 204 resposne' do
get :metrics, deployment_params(id: deployment.id)
expect(response).to have_http_status(204)
expect(response.body).to eq('')
end
end
context 'when environment has some metrics' do
let(:empty_metrics) do
{
success: true,
metrics: {},
last_update: 42
}
end
before do
expect(deployment).to receive(:metrics).and_return(empty_metrics)
end
it 'returns a metrics JSON document' do
get :metrics, deployment_params(id: deployment.id)
expect(response).to be_ok
expect(json_response['success']).to be(true)
expect(json_response['metrics']).to eq({})
expect(json_response['last_update']).to eq(42)
end
end
end
def deployment_params(opts = {})
opts.reverse_merge(namespace_id: project.namespace,
project_id: project,
environment_id: environment.id)
end
end
......@@ -149,6 +149,48 @@ describe Projects::EnvironmentsController do
end
end
describe 'PATCH #stop' do
context 'when env not available' do
it 'returns 404' do
allow_any_instance_of(Environment).to receive(:available?) { false }
patch :stop, environment_params(format: :json)
expect(response).to have_http_status(404)
end
end
context 'when stop action' do
it 'returns action url' do
action = create(:ci_build, :manual)
allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_action!: action)
patch :stop, environment_params(format: :json)
expect(response).to have_http_status(200)
expect(json_response).to eq(
{ 'redirect_url' =>
"http://test.host/#{project.path_with_namespace}/builds/#{action.id}" })
end
end
context 'when no stop action' do
it 'returns env url' do
allow_any_instance_of(Environment)
.to receive_messages(available?: true, stop_with_action!: nil)
patch :stop, environment_params(format: :json)
expect(response).to have_http_status(200)
expect(json_response).to eq(
{ 'redirect_url' =>
"http://test.host/#{project.path_with_namespace}/environments/#{environment.id}" })
end
end
end
describe 'GET #terminal' do
context 'with valid id' do
it 'responds with a status code 200' do
......
......@@ -59,6 +59,18 @@ describe Projects::MergeRequestsController do
end
end
describe 'GET commit_change_content' do
it 'renders commit_change_content template' do
get :commit_change_content,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: 'html'
expect(response).to render_template('_commit_change_content')
end
end
shared_examples "loads labels" do |action|
it "loads labels into the @labels variable" do
get action,
......@@ -71,63 +83,47 @@ describe Projects::MergeRequestsController do
end
describe "GET show" do
shared_examples "export merge as" do |format|
it "does generally work" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: format)
def go(extra_params = {})
params = {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid
}
expect(response).to be_success
end
get :show, params.merge(extra_params)
end
it_behaves_like "loads labels", :show
it_behaves_like "loads labels", :show
it "generates it" do
expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
describe 'as html' do
it "renders merge request page" do
go(format: :html)
get(:show,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: format)
expect(response).to be_success
end
end
it "renders it" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: format)
describe 'as json' do
context 'with basic param' do
it 'renders basic MR entity as json' do
go(basic: true, format: :json)
expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
expect(response).to match_response_schema('entities/merge_request_basic')
end
end
it "does not escape Html" do
allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
and_return('HTML entities &<>" ')
context 'without basic param' do
it 'renders the merge request in the json format' do
go(format: :json)
get(:show,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: format)
expect(response.body).not_to include('&amp;')
expect(response.body).not_to include('&gt;')
expect(response.body).not_to include('&lt;')
expect(response.body).not_to include('&quot;')
expect(response).to match_response_schema('entities/merge_request')
end
end
end
describe "as diff" do
it "triggers workhorse to serve the request" do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: :diff)
go(format: :diff)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
end
......@@ -135,11 +131,7 @@ describe Projects::MergeRequestsController do
describe "as patch" do
it 'triggers workhorse to serve the request' do
get(:show,
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.iid,
format: :patch)
go(format: :patch)
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
end
......@@ -295,19 +287,18 @@ describe Projects::MergeRequestsController do
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
format: 'raw'
format: 'json'
}
end
context 'when the user does not have access' do
context 'when user cannot access' do
before do
project.team.truncate
project.team << [user, :reporter]
post :merge, base_params
project.add_reporter(user)
xhr :post, :merge, base_params
end
it 'returns not found' do
expect(response).to be_not_found
it 'returns 404' do
expect(response).to have_http_status(404)
end
end
......@@ -319,7 +310,7 @@ describe Projects::MergeRequestsController do
end
it 'returns :failed' do
expect(assigns(:status)).to eq(:failed)
expect(json_response).to eq('status' => 'failed')
end
end
......@@ -327,7 +318,7 @@ describe Projects::MergeRequestsController do
before { post :merge, base_params.merge(sha: 'foo') }
it 'returns :sha_mismatch' do
expect(assigns(:status)).to eq(:sha_mismatch)
expect(json_response).to eq('status' => 'sha_mismatch')
end
end
......@@ -339,7 +330,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
expect(assigns(:status)).to eq(:success)
expect(json_response).to eq('status' => 'success')
end
it 'starts the merge immediately' do
......@@ -360,7 +351,7 @@ describe Projects::MergeRequestsController do
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
end
it 'sets the MR to merge when the pipeline succeeds' do
......@@ -382,7 +373,7 @@ describe Projects::MergeRequestsController do
it 'returns :merge_when_pipeline_succeeds' do
merge_when_pipeline_succeeds
expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
end
end
end
......@@ -403,7 +394,7 @@ describe Projects::MergeRequestsController do
it 'returns :failed' do
merge_with_sha
expect(assigns(:status)).to eq(:failed)
expect(json_response).to eq('status' => 'failed')
end
end
......@@ -416,7 +407,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
expect(assigns(:status)).to eq(:success)
expect(json_response).to eq('status' => 'success')
end
end
end
......@@ -434,7 +425,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
expect(assigns(:status)).to eq(:success)
expect(json_response).to eq('status' => 'success')
end
end
......@@ -447,7 +438,7 @@ describe Projects::MergeRequestsController do
it 'returns :success' do
merge_with_sha
expect(assigns(:status)).to eq(:success)
expect(json_response).to eq('status' => 'success')
end
end
end
......@@ -831,18 +822,55 @@ describe Projects::MergeRequestsController do
end
end
context 'POST remove_wip' do
it 'removes the wip status' do
describe 'POST remove_wip' do
before do
merge_request.title = merge_request.wip_title
merge_request.save
post :remove_wip,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
id: merge_request.iid
xhr :post, :remove_wip,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
id: merge_request.iid,
format: :json
end
it 'removes the wip status' do
expect(merge_request.reload.title).to eq(merge_request.wipless_title)
end
it 'renders MergeRequest as JSON' do
expect(json_response.keys).to include('id', 'iid', 'description')
end
end
describe 'POST cancel_merge_when_pipeline_succeeds' do
subject do
xhr :post, :cancel_merge_when_pipeline_succeeds,
namespace_id: merge_request.project.namespace.to_param,
project_id: merge_request.project,
id: merge_request.iid,
format: :json
end
it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
mwps_service = double
allow(MergeRequests::MergeWhenPipelineSucceedsService)
.to receive(:new)
.and_return(mwps_service)
expect(mwps_service).to receive(:cancel).with(merge_request)
subject
end
it { is_expected.to have_http_status(:success) }
it 'renders MergeRequest as JSON' do
subject
expect(json_response.keys).to include('id', 'iid', 'description')
end
end
describe 'GET conflict_for_path' do
......@@ -1121,74 +1149,6 @@ describe Projects::MergeRequestsController do
end
end
describe 'GET merge_widget_refresh' do
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
id: merge_request.iid,
format: :raw
}
end
before do
project.team << [user, :developer]
xhr :get, :merge_widget_refresh, params
end
context 'when merge in progress' do
let(:merge_request) { create(:merge_request, source_project: project, in_progress_merge_commit_sha: 'sha') }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
it 'sets status to :success' do
expect(assigns(:status)).to eq(:success)
expect(response).to render_template('merge')
end
end
context 'when merge request was merged already' do
let(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
it 'sets status to :success' do
expect(assigns(:status)).to eq(:success)
expect(response).to render_template('merge')
end
end
context 'when waiting for build' do
let(:merge_request) { create(:merge_request, source_project: project, merge_when_pipeline_succeeds: true, merge_user: user) }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
it 'sets status to :merge_when_pipeline_succeeds' do
expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
expect(response).to render_template('merge')
end
end
context 'when MR does not have special state' do
let(:merge_request) { create(:merge_request, source_project: project) }
it 'returns an OK response' do
expect(response).to have_http_status(:ok)
end
it 'sets status to success' do
expect(assigns(:status)).to eq(:success)
expect(response).to render_template('merge')
end
end
end
describe 'GET pipeline_status.json' do
context 'when head_pipeline exists' do
let!(:pipeline) do
......
......@@ -38,6 +38,8 @@ describe 'Issue Boards', :feature, :js do
it 'moves un-ordered issue to top of list' do
drag(from_index: 3, to_index: 0)
wait_for_vue_resource
page.within(first('.board')) do
expect(first('.card')).to have_content(issue4.title)
end
......
......@@ -49,7 +49,7 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end
it 'does not show a link to create a new issue' do
expect(page).not_to have_link 'open an issue to resolve them later'
expect(page).not_to have_link 'Create an issue to resolve them later'
end
end
......@@ -59,18 +59,18 @@ feature 'Resolving all open discussions in a merge request from an issue', featu
end
it 'shows a warning that the merge request contains unresolved discussions' do
expect(page).to have_content 'This merge request has unresolved discussions'
expect(page).to have_content 'There are unresolved discussions.'
end
it 'has a link to resolve all discussions by creating an issue' do
page.within '.mr-widget-body' do
expect(page).to have_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
expect(page).to have_link 'Create an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
end
context 'creating an issue for discussions' do
before do
page.click_link 'open an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
page.click_link 'Create an issue to resolve them later', href: new_namespace_project_issue_path(project.namespace, project, merge_request_to_resolve_discussions_of: merge_request.iid)
end
it_behaves_like 'creating an issue for a discussion'
......
......@@ -18,7 +18,7 @@ feature 'Merge request issue assignment', js: true, feature: true do
end
context 'logged in as author' do
scenario 'updates related issues' do
it 'updates related issues' do
visit_merge_request
click_link "Assign yourself to these issues"
......
......@@ -19,8 +19,8 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'does not allow to merge' do
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept merge request'
expect(page).to have_content('This merge request has unresolved discussions')
expect(page).not_to have_button 'Merge'
expect(page).to have_content('There are unresolved discussions.')
end
end
......@@ -32,7 +32,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
expect(page).to have_button 'Merge'
end
end
end
......@@ -46,7 +46,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'does not allow to merge' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
expect(page).to have_button 'Merge'
end
end
......@@ -58,7 +58,7 @@ feature 'Check if mergeable with unresolved discussions', js: true, feature: tru
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
expect(page).to have_button 'Merge'
end
end
end
......
require 'spec_helper'
describe 'Cherry-pick Merge Requests' do
describe 'Cherry-pick Merge Requests', js: true do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
......
require 'spec_helper'
feature 'Merge Request closing issues message', feature: true do
feature 'Merge Request closing issues message', feature: true, js: true do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue_1) { create(:issue, project: project)}
......@@ -23,6 +25,7 @@ feature 'Merge Request closing issues message', feature: true do
login_as user
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
wait_for_ajax
end
context 'not closing or mentioning any issue' do
......@@ -35,7 +38,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
......@@ -51,7 +54,8 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
expect(page).to have_content("Closes issue #{issue_1.to_reference}.")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
end
end
......@@ -59,7 +63,7 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}")
end
end
......@@ -75,7 +79,8 @@ feature 'Merge Request closing issues message', feature: true do
let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
it 'does not display closing issue message' do
expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.")
expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.")
end
end
end
......@@ -31,7 +31,7 @@ feature 'Merge request created from fork' do
fork_project.destroy!
end
scenario 'user can access merge request' do
scenario 'user can access merge request', js: true do
visit_merge_request(merge_request)
expect(page).to have_content 'Test merge request'
......
......@@ -20,7 +20,7 @@ describe 'Deleted source branch', feature: true, js: true do
it 'shows a message about missing source branch' do
expect(page).to have_content(
'Source branch this-branch-does-not-exist does not exist'
'Source branch does not exist.'
)
end
......@@ -35,6 +35,6 @@ describe 'Deleted source branch', feature: true, js: true do
wait_for_ajax
expect(page).to have_selector('.diffs.tab-pane .nothing-here-block')
expect(page).to have_content('Nothing to merge from this-branch-does-not-exist into feature')
expect(page).to have_content('Source branch does not exist.')
end
end
......@@ -29,18 +29,6 @@ feature 'Edit Merge Request', feature: true do
expect(page).to have_content 'Someone edited the merge request the same time you did'
end
it 'allows to unselect "Remove source branch"' do
merge_request.update(merge_params: { 'force_remove_source_branch' => '1' })
expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy
visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
uncheck 'Remove source branch when merge request is accepted'
click_button 'Save changes'
expect(page).to have_content 'Remove source branch'
end
it 'should preserve description textarea height', js: true do
long_description = %q(
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat.
......
......@@ -14,8 +14,6 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
)
end
let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) }
let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) }
let(:default_message) do
[
"Merge branch 'feature' into 'master'",
......@@ -40,7 +38,7 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
expect(textbox).not_to be_visible
expect(page).not_to have_selector('.js-commit-message')
click_button "Modify commit message"
expect(textbox).to be_visible
end
......@@ -56,19 +54,4 @@ feature 'Clicking toggle commit message link', feature: true, js: true do
expect(textbox.value).to eq(default_message)
end
it "toggles link between 'Include description' and 'Don't include description'" do
expect(include_link).to be_visible
expect(do_not_include_link).not_to be_visible
click_link "Include description in commit message"
expect(include_link).not_to be_visible
expect(do_not_include_link).to be_visible
click_link "Don't include description in commit message"
expect(include_link).to be_visible
expect(do_not_include_link).not_to be_visible
end
end
......@@ -34,7 +34,7 @@ feature 'Merge immediately', :feature, :js do
click_link 'Merge immediately'
expect(find('.js-merge-when-pipeline-succeeds-button')).to have_content('Merge in progress')
expect(find('.accept-merge-request.btn-info')).to have_content('Merge in progress')
wait_for_ajax
end
......
......@@ -38,8 +38,8 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
click_button "Merge when pipeline succeeds"
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
expect(page).to have_content "The source branch will not be removed."
expect(page).to have_link "Cancel automatic merge"
expect(page).to have_content "The source branch will be removed."
expect(page).to have_selector ".js-cancel-auto-merge"
visit_merge_request(merge_request) # Needed to refresh the page
expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i
end
......@@ -93,12 +93,10 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
describe 'enabling Merge when pipeline succeeds via dropdown' do
it 'activates the Merge when pipeline succeeds feature' do
click_button 'Select merge moment'
within('.js-merge-dropdown') do
click_link 'Merge when pipeline succeeds'
end
click_link 'Merge when pipeline succeeds'
expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds."
expect(page).to have_content "The source branch will not be removed."
expect(page).to have_content "The source branch will be removed."
expect(page).to have_link "Cancel automatic merge"
end
end
......@@ -131,13 +129,6 @@ feature 'Merge When Pipeline Succeeds', :feature, :js do
expect(page).to have_content "canceled the automatic merge"
end
it "allows the user to remove the source branch" do
expect(page).to have_link "Remove source branch when merged"
click_link "Remove source branch when merged"
expect(page).to have_content "The source branch will be removed"
end
context 'when pipeline succeeds' do
background { build.success }
......
require 'spec_helper'
feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true do
feature 'Only allow merge requests to be merged if the pipeline succeeds', feature: true, js: true do
include WaitForVueResource
let(:merge_request) { create(:merge_request_with_diffs) }
let(:project) { merge_request.target_project }
......@@ -10,15 +12,17 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
project.team << [merge_request.author, :master]
end
context 'project does not have CI enabled' do
context 'project does not have CI enabled', js: true do
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).to have_button 'Merge'
end
end
context 'when project has CI enabled' do
context 'when project has CI enabled', js: true do
given!(:pipeline) do
create(:ci_empty_pipeline,
project: project,
......@@ -38,6 +42,8 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'does not allow to merge immediately' do
visit_merge_request(merge_request)
wait_for_vue_resource
expect(page).to have_button 'Merge when pipeline succeeds'
expect(page).not_to have_button 'Select merge moment'
end
......@@ -49,7 +55,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'does not allow MR to be merged' do
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).to have_css('button[disabled="disabled"]', text: 'Merge')
expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
......@@ -60,7 +68,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'does not allow MR to be merged' do
visit_merge_request(merge_request)
expect(page).not_to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).not_to have_button 'Merge'
expect(page).to have_content('Please retry the job or push a new commit to fix the failure.')
end
end
......@@ -71,7 +81,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).to have_button 'Merge'
end
end
......@@ -81,7 +93,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).to have_button 'Merge'
end
end
end
......@@ -94,9 +108,11 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
context 'when CI is running' do
given(:status) { :running }
it 'allows MR to be merged immediately', js: true do
it 'allows MR to be merged immediately' do
visit_merge_request(merge_request)
wait_for_vue_resource
expect(page).to have_button 'Merge when pipeline succeeds'
click_button 'Select merge moment'
......@@ -110,7 +126,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).to have_button 'Merge'
end
end
......@@ -120,7 +138,9 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', featu
it 'allows MR to be merged' do
visit_merge_request(merge_request)
expect(page).to have_button 'Accept merge request'
wait_for_vue_resource
expect(page).to have_button 'Merge'
end
end
end
......
require 'spec_helper'
describe 'Target branch', feature: true do
describe 'Target branch', feature: true, js: true do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
......@@ -17,11 +17,6 @@ describe 'Target branch', feature: true do
project.team << [user, :master]
end
it 'shows link to target branch' do
visit path_to_merge_request
expect(page).to have_link('feature', href: namespace_project_commits_path(project.namespace, project, merge_request.target_branch))
end
context 'when branch was deleted' do
before do
DeleteBranchService.new(project, user).execute('feature')
......@@ -30,12 +25,12 @@ describe 'Target branch', feature: true do
it 'shows a message about missing target branch' do
expect(page).to have_content(
'Target branch feature does not exist'
'Target branch does not exist'
)
end
it 'does not show link to target branch' do
expect(page).not_to have_link('feature')
expect(page).not_to have_selector('.mr-widget-body .js-branch-text a')
end
end
end
......@@ -21,7 +21,7 @@ feature 'Widget Deployments Header', feature: true, js: true do
wait_for_ajax
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.ci_widget > span > span')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
expect(find('.js-deploy-time')['data-title']).to eq(deployment.created_at.to_time.in_time_zone.to_s(:medium))
end
context 'with stop action' do
......@@ -38,11 +38,11 @@ feature 'Widget Deployments Header', feature: true, js: true do
end
scenario 'does show stop button' do
expect(page).to have_link('Stop environment')
expect(page).to have_button('Stop environment')
end
scenario 'does start build when stop button clicked' do
click_link('Stop environment')
click_button('Stop environment')
expect(page).to have_content('close_app')
end
......@@ -51,7 +51,7 @@ feature 'Widget Deployments Header', feature: true, js: true do
given(:role) { :reporter }
scenario 'does not show stop button' do
expect(page).not_to have_link('Stop environment')
expect(page).not_to have_button('Stop environment')
end
end
end
......
......@@ -30,6 +30,7 @@ describe 'Merge request', :feature, :js do
wait_for_ajax
expect(page).to have_selector('.accept-merge-request')
expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
end
......@@ -51,14 +52,15 @@ describe 'Merge request', :feature, :js do
page.within('.mr-widget-heading') do
expect(page).to have_content("Deployed to #{environment.name}")
expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url)
expect(find('.js-deploy-url')[:href]).to include(environment.formatted_external_url)
end
end
it 'shows green accept merge request button' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_ajax
expect(page).to have_selector('.accept-merge-request.btn-create')
expect(page).to have_selector('.accept-merge-request')
expect(find('.accept-merge-request')['disabled']).not_to be(true)
end
end
......@@ -135,7 +137,28 @@ describe 'Merge request', :feature, :js do
it 'has info button when MWBS button' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_ajax
expect(page).to have_selector('.merge-when-pipeline-succeeds.btn-info')
expect(page).to have_selector('.accept-merge-request.btn-info')
end
end
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
merge_when_pipeline_succeeds: true,
merge_user: merge_request.author,
merge_error: 'Something went wrong'
)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'shows information about the merge error' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_ajax
page.within('.mr-widget-body') do
expect(page).to have_content('Something went wrong')
end
end
end
......@@ -164,11 +187,11 @@ describe 'Merge request', :feature, :js do
before do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
click_button 'Accept merge request'
wait_for_ajax
end
it 'updates the MR widget' do
click_button 'Merge'
page.within('.mr-widget-body') do
expect(page).to have_content('Conflicts detected during merge')
end
......
{
"type": "object",
"properties" : {
"id": { "type": "integer" },
"iid": { "type": "integer" },
"assignee_id": { "type": ["integer", "null"] },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"lock_version": { "type": ["string", "null"] },
"milestone_id": { "type": ["string", "null"] },
"position": { "type": "integer" },
"state": { "type": "string" },
"title": { "type": "string" },
"updated_by_id": { "type": ["string", "null"] },
"created_at": { "type": "string" },
"updated_at": { "type": "string" },
"deleted_at": { "type": ["string", "null"] },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["integer", "null"] },
"human_total_time_spent": { "type": ["integer", "null"] },
"in_progress_merge_commit_sha": { "type": ["string", "null"] },
"locked_at": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] },
"merge_commit_sha": { "type": ["string", "null"] },
"merge_params": { "type": ["object", "null"] },
"merge_status": { "type": "string" },
"merge_user_id": { "type": ["integer", "null"] },
"merge_when_pipeline_succeeds": { "type": "boolean" },
"source_branch": { "type": "string" },
"source_project_id": { "type": "integer" },
"target_branch": { "type": "string" },
"target_project_id": { "type": "integer" },
"merge_event": { "type": ["object", "null"] },
"closed_event": { "type": ["object", "null"] },
"author": { "type": ["object", "null"] },
"merge_user": { "type": ["object", "null"] },
"diff_head_sha": { "type": ["string", "null"] },
"diff_head_commit_short_id": { "type": ["string", "null"] },
"merge_commit_message": { "type": ["string", "null"] },
"pipeline": { "type": ["object", "null"] },
"work_in_progress": { "type": "boolean" },
"source_branch_exists": { "type": "boolean" },
"mergeable_discussions_state": { "type": "boolean" },
"conflicts_can_be_resolved_in_ui": { "type": "boolean" },
"branch_missing": { "type": "boolean" },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
"project_archived": { "type": "boolean" },
"only_allow_merge_if_pipeline_succeeds": { "type": "boolean" },
"has_ci": { "type": "boolean" },
"ci_status": { "type": ["string", "null"] },
"issues_links": {
"type": "object",
"required": ["closing", "mentioned_but_not_closing", "assign_to_closing"],
"properties" : {
"closing": { "type": "string" },
"mentioned_but_not_closing": { "type": "string" },
"assign_to_closing": { "type": ["string", "null"] }
},
"additionalProperties": false
},
"source_branch_with_namespace_link": { "type": "string" },
"current_user": {
"type": "object",
"required": [
"can_remove_source_branch",
"can_revert_on_current_merge_request",
"can_cherry_pick_on_current_merge_request"
],
"properties": {
"can_remove_source_branch": { "type": "boolean" },
"can_revert_on_current_merge_request": { "type": ["boolean", "null"] },
"can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] }
},
"additionalProperties": false
},
"target_branch_commits_path": { "type": "string" },
"source_branch_path": { "type": "string" },
"conflict_resolution_path": { "type": ["string", "null"] },
"cancel_merge_when_pipeline_succeeds_path": { "type": "string" },
"create_issue_to_resolve_discussions_path": { "type": "string" },
"merge_path": { "type": "string" },
"cherry_pick_in_fork_path": { "type": ["string", "null"] },
"revert_in_fork_path": { "type": ["string", "null"] },
"email_patches_path": { "type": "string" },
"plain_diff_path": { "type": "string" },
"status_path": { "type": "string" },
"merge_check_path": { "type": "string" },
"ci_environments_status_path": { "type": "string" },
"merge_commit_message_with_description": { "type": "string" },
"diverged_commits_count": { "type": "integer" },
"commit_change_content_path": { "type": "string" },
"remove_wip_path": { "type": "string" },
"commits_count": { "type": "integer" }
},
"additionalProperties": false
}
{
"type": "object",
"properties" : {
"state": { "type": "string" },
"merge_status": { "type": "string" },
"source_branch_exists": { "type": "boolean" },
"time_estimate": { "type": "integer" },
"total_time_spent": { "type": "integer" },
"human_time_estimate": { "type": ["string", "null"] },
"human_total_time_spent": { "type": ["string", "null"] },
"merge_error": { "type": ["string", "null"] }
},
"additionalProperties": false
}
......@@ -21,55 +21,6 @@ describe MergeRequestsHelper do
end
end
describe '#issues_sentence' do
let(:project) { create :project }
subject { issues_sentence(issues) }
let(:issues) do
[build(:issue, iid: 2, project: project),
build(:issue, iid: 3, project: project),
build(:issue, iid: 1, project: project)]
end
it do
@project = project
is_expected.to eq('#1, #2, and #3')
end
context 'for JIRA issues' do
let(:project) { create(:empty_project) }
let(:issues) do
[
ExternalIssue.new('JIRA-456', project),
ExternalIssue.new('FOOBAR-7890', project),
ExternalIssue.new('JIRA-123', project)
]
end
it do
@project = project
is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456')
end
end
context 'for issues from multiple namespaces' do
let(:project) { create(:project) }
let(:other_project) { create(:project) }
let(:issues) do
[build(:issue, iid: 2, project: project),
build(:issue, iid: 3, project: other_project),
build(:issue, iid: 1, project: project)]
end
it do
@project = project
is_expected.to eq("#1, #2, and #{other_project.namespace.path}/#{other_project.path}#3")
end
end
end
describe '#format_mr_branch_names' do
describe 'within the same project' do
let(:merge_request) { create(:merge_request) }
......@@ -89,147 +40,4 @@ describe MergeRequestsHelper do
it { is_expected.to eq([source_title, target_title]) }
end
end
describe '#mr_widget_refresh_url' do
let(:guest) { create(:user) }
let(:project) { create(:project, :public) }
let(:project_fork) { Projects::ForkService.new(project, guest).execute }
let(:merge_request) { create(:merge_request, source_project: project_fork, target_project: project) }
it 'returns correct url for MR' do
expected_url = "#{project.path_with_namespace}/merge_requests/#{merge_request.iid}/merge_widget_refresh"
expect(mr_widget_refresh_url(merge_request)).to end_with(expected_url)
end
it 'returns empty string for nil' do
expect(mr_widget_refresh_url(nil)).to eq('')
end
end
describe '#mr_closes_issues' do
let(:user_1) { create(:user) }
let(:user_2) { create(:user) }
let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
let(:issue_1) { create(:issue, project: project_1) }
let(:issue_2) { create(:issue, project: project_2) }
let(:merge_request) { create(:merge_request, source_project: project_1, target_project: project_1,) }
let(:merge_request) do
create(:merge_request,
source_project: project_1, target_project: project_1,
description: "Fixes #{issue_1.to_reference} Fixes #{issue_2.to_reference(project_1)}")
end
before do
project_1.team << [user_2, :developer]
project_2.team << [user_2, :developer]
allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
@merge_request = merge_request
end
context 'user without access to another private project' do
let(:current_user) { user_1 }
it 'cannot see that project\'s issue that will be closed on acceptance' do
expect(mr_closes_issues).to contain_exactly(issue_1)
end
end
context 'user with access to another private project' do
let(:current_user) { user_2 }
it 'can see that project\'s issue that will be closed on acceptance' do
expect(mr_closes_issues).to contain_exactly(issue_1, issue_2)
end
end
end
describe '#target_projects' do
let(:project) { create(:empty_project) }
let(:fork_project) { create(:empty_project, forked_from_project: project) }
context 'when target project has enabled merge requests' do
it 'returns the forked_from project' do
expect(target_projects(fork_project)).to contain_exactly(project, fork_project)
end
end
context 'when target project has disabled merge requests' do
it 'returns the forked project' do
project.project_feature.update(merge_requests_access_level: 0)
expect(target_projects(fork_project)).to contain_exactly(fork_project)
end
end
end
describe '#new_mr_path_from_push_event' do
subject(:url_params) { URI.decode_www_form(new_mr_path_from_push_event(event)).to_h }
let(:user) { create(:user) }
let(:project) { create(:empty_project, creator: user) }
let(:fork_project) { create(:project, forked_from_project: project, creator: user) }
let(:event) do
push_data = Gitlab::DataBuilder::Push.build_sample(fork_project, user)
create(:event, :pushed, project: fork_project, target: fork_project, author: user, data: push_data)
end
context 'when target project has enabled merge requests' do
it 'returns link to create merge request on source project' do
expect(url_params['merge_request[target_project_id]'].to_i).to eq(project.id)
end
end
context 'when target project has disabled merge requests' do
it 'returns link to create merge request on forked project' do
project.project_feature.update(merge_requests_access_level: 0)
expect(url_params['merge_request[target_project_id]'].to_i).to eq(fork_project.id)
end
end
end
describe '#mr_issues_mentioned_but_not_closing' do
let(:user_1) { create(:user) }
let(:user_2) { create(:user) }
let(:project_1) { create(:project, :private, creator: user_1, namespace: user_1.namespace) }
let(:project_2) { create(:project, :private, creator: user_2, namespace: user_2.namespace) }
let(:issue_1) { create(:issue, project: project_1) }
let(:issue_2) { create(:issue, project: project_2) }
let(:merge_request) do
create(:merge_request,
source_project: project_1, target_project: project_1,
description: "#{issue_1.to_reference} #{issue_2.to_reference(project_1)}")
end
before do
project_1.team << [user_2, :developer]
project_2.team << [user_2, :developer]
allow(merge_request.project).to receive(:default_branch).and_return(merge_request.target_branch)
@merge_request = merge_request
end
context 'user without access to another private project' do
let(:current_user) { user_1 }
it 'cannot see that project\'s issue that will be closed on acceptance' do
expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1)
end
end
context 'user with access to another private project' do
let(:current_user) { user_2 }
it 'can see that project\'s issue that will be closed on acceptance' do
expect(mr_issues_mentioned_but_not_closing).to contain_exactly(issue_1, issue_2)
end
end
end
end
......@@ -61,6 +61,7 @@ export default {
tag: false,
branch: true,
},
coverage: '42.21',
commit: {
id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
short_id: 'fbd79f04',
......
......@@ -16,6 +16,16 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
sha: merge_request.diff_head_sha
)
end
let(:path) { "files/ruby/popen.rb" }
let(:position) do
Gitlab::Diff::Position.new(
old_path: path,
new_path: path,
old_line: nil,
new_line: 14,
diff_refs: merge_request.diff_refs
)
end
render_views
......@@ -39,6 +49,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request(example.description, merged_merge_request)
end
it 'merge_requests/diff_comment.html.raw' do |example|
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
render_merge_request(example.description, merge_request)
end
private
def render_merge_request(fixture_file_name, merge_request)
......
/* global Notes */
import 'vendor/autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
import '~/render_math';
import '~/notes';
describe('Merge request notes', () => {
window.gon = window.gon || {};
window.gl = window.gl || {};
gl.utils = gl.utils || {};
const fixture = 'merge_requests/diff_comment.html.raw';
preloadFixtures(fixture);
beforeEach(() => {
loadFixtures(fixture);
gl.utils.disableButtonIfEmptyField = _.noop;
window.project_uploads_path = 'http://test.host/uploads';
$('body').data('page', 'projects:merge_requests:show');
window.gon.current_user_id = $('.note:last').data('author-id');
return new Notes('', []);
});
describe('up arrow', () => {
it('edits last comment when triggered in main form', () => {
const upArrowEvent = $.Event('keydown');
upArrowEvent.which = 38;
spyOnEvent('.note:last .js-note-edit', 'click');
$('.js-note-text').trigger(upArrowEvent);
expect('click').toHaveBeenTriggeredOn('.note:last .js-note-edit');
});
it('edits last comment in discussion when triggered in discussion form', (done) => {
const upArrowEvent = $.Event('keydown');
upArrowEvent.which = 38;
spyOnEvent('.note-discussion .js-note-edit', 'click');
$('.js-discussion-reply-button').click();
setTimeout(() => {
expect(
$('.note-discussion .js-note-text'),
).toExist();
$('.note-discussion .js-note-text').trigger(upArrowEvent);
expect('click').toHaveBeenTriggeredOn('.note-discussion .js-note-edit');
done();
});
});
});
});
/* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */
require('~/merge_request_widget');
require('~/smart_interval');
require('~/lib/utils/datetime_utility');
(function() {
describe('MergeRequestWidget', function() {
beforeEach(function() {
window.notifyPermissions = function() {};
window.notify = function() {};
this.opts = {
ci_status_url: "http://sampledomain.local/ci/getstatus",
ci_environments_status_url: "http://sampledomain.local/ci/getenvironmentsstatus",
ci_status: "",
ci_message: {
normal: "Build {{status}} for \"{{title}}\"",
preparing: "{{status}} build for \"{{title}}\""
},
ci_title: {
preparing: "{{status}} build",
normal: "Build {{status}}"
},
gitlab_icon: "gitlab_logo.png",
ci_pipeline: 80,
ci_sha: "12a34bc5",
builds_path: "http://sampledomain.local/sampleBuildsPath",
commits_path: "http://sampledomain.local/commits",
pipeline_path: "http://sampledomain.local/pipelines"
};
this["class"] = new window.gl.MergeRequestWidget(this.opts);
});
describe('getCIEnvironmentsStatus', function() {
beforeEach(function() {
this.ciEnvironmentsStatusData = [{
created_at: '2016-09-12T13:38:30.636Z',
environment_id: 1,
environment_name: 'env1',
external_url: 'https://test-url.com',
external_url_formatted: 'test-url.com'
}];
spyOn(jQuery, 'getJSON').and.callFake(function(req, cb) {
cb(this.ciEnvironmentsStatusData);
}.bind(this));
});
it('should call renderEnvironments when the environments property is set', function() {
const spy = spyOn(this.class, 'renderEnvironments').and.stub();
this.class.getCIEnvironmentsStatus();
expect(spy).toHaveBeenCalledWith(this.ciEnvironmentsStatusData);
});
it('should not call renderEnvironments when the environments property is not set', function() {
this.ciEnvironmentsStatusData = null;
const spy = spyOn(this.class, 'renderEnvironments').and.stub();
this.class.getCIEnvironmentsStatus();
expect(spy).not.toHaveBeenCalled();
});
});
describe('renderEnvironments', function() {
describe('should render correct timeago', function() {
beforeEach(function() {
this.environments = [{
id: 'test-environment-id',
url: 'testurl',
deployed_at: new Date().toISOString(),
deployed_at_formatted: true
}];
});
function getTimeagoText(template) {
var el = document.createElement('html');
el.innerHTML = template;
return el.querySelector('.js-environment-timeago').innerText.trim();
}
it('should render less than a minute ago text', function() {
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('less than a minute ago.');
});
this.class.renderEnvironments(this.environments);
});
it('should render about an hour ago text', function() {
var oneHourAgo = new Date();
oneHourAgo.setHours(oneHourAgo.getHours() - 1);
this.environments[0].deployed_at = oneHourAgo.toISOString();
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('about an hour ago.');
});
this.class.renderEnvironments(this.environments);
});
it('should render about 2 hours ago text', function() {
var twoHoursAgo = new Date();
twoHoursAgo.setHours(twoHoursAgo.getHours() - 2);
this.environments[0].deployed_at = twoHoursAgo.toISOString();
spyOn(this.class.$widgetBody, 'before').and.callFake(function(template) {
expect(getTimeagoText(template)).toBe('about 2 hours ago.');
});
this.class.renderEnvironments(this.environments);
});
});
});
describe('mergeInProgress', function() {
it('should display error with h4 tag', function() {
spyOn(this.class.$widgetBody, 'html').and.callFake(function(html) {
expect(html).toBe('<h4>Sorry, something went wrong.</h4>');
});
spyOn($, 'ajax').and.callFake(function(e) {
e.success({ merge_error: 'Sorry, something went wrong.' });
});
this.class.mergeInProgress(null);
});
});
describe('getCIStatus', function() {
beforeEach(function() {
this.ciStatusData = {
"title": "Sample MR title",
"pipeline": 80,
"sha": "12a34bc5",
"status": "success",
"coverage": 98
};
spyOn(jQuery, 'getJSON').and.callFake((function(_this) {
return function(req, cb) {
return cb(_this.ciStatusData);
};
})(this));
});
it('should call showCIStatus even if a notification should not be displayed', function() {
var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
});
it('should call showCIStatus when a notification should be displayed', function() {
var spy;
spy = spyOn(this["class"], 'showCIStatus').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(true);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status);
});
it('should call showCICoverage when the coverage rate is set', function() {
var spy;
spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage);
});
it('should not call showCICoverage when the coverage rate is not set', function() {
var spy;
this.ciStatusData.coverage = null;
spy = spyOn(this["class"], 'showCICoverage').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
return expect(spy).not.toHaveBeenCalled();
});
it('should not display a notification on the first check after the widget has been created', function() {
var spy;
spy = spyOn(window, 'notify');
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"] = new window.gl.MergeRequestWidget(this.opts);
this["class"].getCIStatus(true);
return expect(spy).not.toHaveBeenCalled();
});
it('should update the pipeline URL when the pipeline changes', function() {
var spy;
spy = spyOn(this["class"], 'updatePipelineUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
this.ciStatusData.pipeline += 1;
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalled();
});
it('should update the commit URL when the sha changes', function() {
var spy;
spy = spyOn(this["class"], 'updateCommitUrls').and.stub();
spyOn(gl.utils, 'setCiStatusFavicon').and.callFake(() => {});
this["class"].getCIStatus(false);
this.ciStatusData.sha = "9b50b99a";
this["class"].getCIStatus(false);
return expect(spy).toHaveBeenCalled();
});
});
});
}).call(window);
/* global MergedButtons */
import '~/merged_buttons';
describe('MergedButtons', () => {
const fixturesPath = 'merge_requests/merged_merge_request.html.raw';
preloadFixtures(fixturesPath);
beforeEach(() => {
loadFixtures(fixturesPath);
this.mergedButtons = new MergedButtons();
this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)');
this.$removeBranchProgress = $('.remove_source_branch_in_progress');
this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
this.$removeBranchButton = $('.remove_source_branch');
});
describe('removeSourceBranch', () => {
it('shows loader', () => {
$('.remove_source_branch').trigger('click');
expect(this.$removeBranchProgress).toBeVisible();
expect(this.$removeBranchWidget).not.toBeVisible();
});
});
describe('removeBranchSuccess', () => {
it('refreshes page when branch removed', () => {
spyOn(gl.utils, 'refreshCurrentPage').and.stub();
const response = { status: 200 };
this.$removeBranchButton.trigger('ajax:success', response, 'xhr');
expect(gl.utils.refreshCurrentPage).toHaveBeenCalled();
});
});
describe('removeBranchError', () => {
it('shows error message', () => {
const response = { status: 500 };
this.$removeBranchButton.trigger('ajax:error', response, 'xhr');
expect(this.$removeBranchFailed).toBeVisible();
expect(this.$removeBranchProgress).not.toBeVisible();
expect(this.$removeBranchWidget).not.toBeVisible();
});
});
});
......@@ -55,7 +55,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./merge_conflicts/merge_conflicts_bundle.js',
'./merge_conflicts/components/inline_conflict_lines.js',
'./merge_conflicts/components/parallel_conflict_lines.js',
'./merge_request_widget/ci_bundle.js',
'./monitoring/monitoring_bundle.js',
'./network/network_bundle.js',
'./network/branch_graph.js',
......
import Vue from 'vue';
import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
const author = {
webUrl: 'http://foo.bar',
avatarUrl: 'http://gravatar.com/foo',
name: 'fatihacet',
};
const createComponent = () => {
const Component = Vue.extend(authorComponent);
return new Component({
el: document.createElement('div'),
propsData: { author },
});
};
describe('MRWidgetAuthor', () => {
describe('props', () => {
it('should have props', () => {
const authorProp = authorComponent.props.author;
expect(authorProp).toBeDefined();
expect(authorProp.type instanceof Object).toBeTruthy();
expect(authorProp.required).toBeTruthy();
});
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
expect(el.tagName).toEqual('A');
expect(el.getAttribute('href')).toEqual(author.webUrl);
expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
expect(el.querySelector('.author').innerText.trim()).toEqual(author.name);
});
});
});
import Vue from 'vue';
import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
const props = {
actionText: 'Merged by',
author: {
webUrl: 'http://foo.bar',
avatarUrl: 'http://gravatar.com/foo',
name: 'fatihacet',
},
dateTitle: '2017-03-23T23:02:00.807Z',
dateReadable: '12 hours ago',
};
const createComponent = () => {
const Component = Vue.extend(authorTimeComponent);
return new Component({
el: document.createElement('div'),
propsData: props,
});
};
describe('MRWidgetAuthorTime', () => {
describe('props', () => {
it('should have props', () => {
const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
const ActionTextClass = actionText.type;
const DateTitleClass = dateTitle.type;
const DateReadableClass = dateReadable.type;
expect(new ActionTextClass() instanceof String).toBeTruthy();
expect(actionText.required).toBeTruthy();
expect(author.type instanceof Object).toBeTruthy();
expect(author.required).toBeTruthy();
expect(new DateTitleClass() instanceof String).toBeTruthy();
expect(dateTitle.required).toBeTruthy();
expect(new DateReadableClass() instanceof String).toBeTruthy();
expect(dateReadable.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components', () => {
expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
});
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
expect(el.tagName).toEqual('H4');
expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
expect(el.querySelector('time').innerText).toContain(props.dateReadable);
expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
});
});
});
import Vue from 'vue';
import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
const deploymentMockData = [
{
id: 15,
name: 'review/diplo',
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
external_url: 'http://diplo.',
external_url_formatted: 'diplo.',
deployed_at: '2017-03-22T22:44:42.258Z',
deployed_at_formatted: 'Mar 22, 2017 10:44pm',
},
];
const createComponent = () => {
const Component = Vue.extend(deploymentComponent);
const mr = {
deployments: deploymentMockData,
};
const service = {};
return new Component({
el: document.createElement('div'),
propsData: { mr, service },
});
};
describe('MRWidgetDeployment', () => {
describe('props', () => {
it('should have props', () => {
const { mr, service } = deploymentComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
expect(service.type instanceof Object).toBeTruthy();
expect(service.required).toBeTruthy();
});
});
describe('computed', () => {
describe('svg', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent(deploymentMockData);
expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success);
});
});
});
describe('methods', () => {
let vm = createComponent();
const deployment = deploymentMockData[0];
describe('formatDate', () => {
it('should work', () => {
const readable = gl.utils.getTimeago().format(deployment.deployed_at);
expect(vm.formatDate(deployment.deployed_at)).toEqual(readable);
});
});
describe('hasExternalUrls', () => {
it('should return true', () => {
expect(vm.hasExternalUrls(deployment)).toBeTruthy();
});
it('should return false when there is not enough information', () => {
expect(vm.hasExternalUrls()).toBeFalsy();
expect(vm.hasExternalUrls({ external_url: 'Diplo' })).toBeFalsy();
expect(vm.hasExternalUrls({ external_url_formatted: 'Diplo' })).toBeFalsy();
});
});
describe('hasDeploymentTime', () => {
it('should return true', () => {
expect(vm.hasDeploymentTime(deployment)).toBeTruthy();
});
it('should return false when there is not enough information', () => {
expect(vm.hasDeploymentTime()).toBeFalsy();
expect(vm.hasDeploymentTime({ deployed_at: 'Diplo' })).toBeFalsy();
expect(vm.hasDeploymentTime({ deployed_at_formatted: 'Diplo' })).toBeFalsy();
});
});
describe('hasDeploymentMeta', () => {
it('should return true', () => {
expect(vm.hasDeploymentMeta(deployment)).toBeTruthy();
});
it('should return false when there is not enough information', () => {
expect(vm.hasDeploymentMeta()).toBeFalsy();
expect(vm.hasDeploymentMeta({ url: 'Diplo' })).toBeFalsy();
expect(vm.hasDeploymentMeta({ name: 'Diplo' })).toBeFalsy();
});
});
describe('stopEnvironment', () => {
const url = '/foo/bar';
const returnPromise = () => new Promise((resolve) => {
resolve({
json() {
return {
redirect_url: url,
};
},
});
});
const mockStopEnvironment = () => {
vm.stopEnvironment(deploymentMockData);
return vm;
};
it('should show a confirm dialog and call service.stopEnvironment when confirmed', (done) => {
spyOn(window, 'confirm').and.returnValue(true);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(true));
spyOn(gl.utils, 'visitUrl').and.returnValue(true);
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).toHaveBeenCalledWith(deploymentMockData.stop_url);
setTimeout(() => {
expect(gl.utils.visitUrl).toHaveBeenCalledWith(url);
done();
}, 333);
});
it('should show a confirm dialog but should not work if the dialog is rejected', () => {
spyOn(window, 'confirm').and.returnValue(false);
spyOn(MRWidgetService, 'stopEnvironment').and.returnValue(returnPromise(false));
vm = mockStopEnvironment();
expect(window.confirm).toHaveBeenCalled();
expect(MRWidgetService.stopEnvironment).not.toHaveBeenCalled();
});
});
});
describe('template', () => {
let vm;
let el;
const [deployment] = deploymentMockData;
beforeEach(() => {
vm = createComponent(deploymentMockData);
el = vm.$el;
});
it('should render template elements correctly', () => {
expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
expect(el.querySelector('.js-icon-link')).toBeDefined();
expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(deployment.url);
expect(el.querySelector('.js-deploy-meta').innerText).toContain(deployment.name);
expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url);
expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted);
expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at));
expect(el.querySelector('button')).toBeDefined();
});
it('should list multiple deployments', (done) => {
vm.mr.deployments.push(deployment);
vm.mr.deployments.push(deployment);
Vue.nextTick(() => {
expect(el.querySelectorAll('.ci-widget').length).toEqual(3);
done();
});
});
it('should not have some elements when there is not enough data', (done) => {
vm.mr.deployments = [{}];
Vue.nextTick(() => {
expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0);
expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0);
expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0);
expect(el.querySelectorAll('.button').length).toEqual(0);
done();
});
});
});
});
import Vue from 'vue';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
const createComponent = (mr) => {
const Component = Vue.extend(headerComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
describe('MRWidgetHeader', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = headerComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
});
describe('computed', () => {
let vm;
beforeEach(() => {
vm = createComponent({
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '/foo/bar/mr-widget-refactor',
targetBranch: 'master',
});
});
it('shouldShowCommitsBehindText', () => {
expect(vm.shouldShowCommitsBehindText).toBeTruthy();
vm.mr.divergedCommitsCount = 0;
expect(vm.shouldShowCommitsBehindText).toBeFalsy();
});
it('commitsText', () => {
expect(vm.commitsText).toEqual('commits');
vm.mr.divergedCommitsCount = 1;
expect(vm.commitsText).toEqual('commit');
});
});
describe('template', () => {
let vm;
let el;
const mr = {
divergedCommitsCount: 12,
sourceBranch: 'mr-widget-refactor',
sourceBranchLink: '/foo/bar/mr-widget-refactor',
targetBranch: 'master',
isOpen: true,
emailPatchesPath: '/mr/email-patches',
plainDiffPath: '/mr/plainDiffPath',
};
beforeEach(() => {
vm = createComponent(mr);
el = vm.$el;
});
it('should render template elements correctly', () => {
expect(el.classList.contains('mr-source-target')).toBeTruthy();
expect(el.querySelectorAll('.label-branch')[0].textContent).toContain(mr.sourceBranch);
expect(el.querySelectorAll('.label-branch')[1].textContent).toContain(mr.targetBranch);
expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind');
expect(el.textContent).toContain('Check out branch');
expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
});
it('should not have right action links if the MR state is not open', (done) => {
vm.mr.isOpen = false;
Vue.nextTick(() => {
expect(el.textContent).not.toContain('Check out branch');
expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
done();
});
});
it('should not render diverged commits count if the MR has no diverged commits', (done) => {
vm.mr.divergedCommitsCount = null;
Vue.nextTick(() => {
expect(el.textContent).not.toContain('commits behind');
expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
done();
});
});
});
});
import Vue from 'vue';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
const props = {
missingBranch: 'this-is-not-the-branch-you-are-looking-for',
};
const text = `If the ${props.missingBranch} branch exists in your local repository`;
const createComponent = () => {
const Component = Vue.extend(mergeHelpComponent);
return new Component({
el: document.createElement('div'),
propsData: props,
});
};
describe('MRWidgetMergeHelp', () => {
describe('props', () => {
it('should have props', () => {
const { missingBranch } = mergeHelpComponent.props;
const MissingBranchTypeClass = missingBranch.type;
expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
expect(missingBranch.required).toBeFalsy();
expect(missingBranch.default).toEqual('');
});
});
describe('template', () => {
let vm;
let el;
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
it('should have the correct elements', () => {
expect(el.classList.contains('mr-widget-help')).toBeTruthy();
expect(el.textContent).toContain(text);
});
it('should not show missing branch name if missingBranch props is not provided', (done) => {
vm.missingBranch = null;
Vue.nextTick(() => {
expect(el.textContent).not.toContain(text);
done();
});
});
});
});
import Vue from 'vue';
import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
import mockData from '../mock_data';
const createComponent = (mr) => {
const Component = Vue.extend(pipelineComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
describe('MRWidgetPipeline', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = pipelineComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components added', () => {
expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined();
});
});
describe('computed', () => {
describe('svg', () => {
it('should have the proper SVG icon', () => {
const vm = createComponent({ pipeline: mockData.pipeline });
expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed);
});
});
describe('hasCIError', () => {
it('should return false when there is no CI error', () => {
const vm = createComponent({
pipeline: mockData.pipeline,
hasCI: true,
ciStatus: 'success',
});
expect(vm.hasCIError).toBeFalsy();
});
it('should return true when there is a CI error', () => {
const vm = createComponent({
pipeline: mockData.pipeline,
hasCI: true,
ciStatus: null,
});
expect(vm.hasCIError).toBeTruthy();
});
});
});
describe('template', () => {
let vm;
let el;
const { pipeline } = mockData;
const mr = {
hasCI: true,
ciStatus: 'success',
pipelineDetailedStatus: pipeline.details.status,
pipeline,
};
beforeEach(() => {
vm = createComponent(mr);
el = vm.$el;
});
it('should render template elements correctly', () => {
expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1);
expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`);
expect(el.innerText).toContain('passed');
expect(el.innerText).toContain('with stages');
expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path);
expect(el.querySelectorAll('.stage-container').length).toEqual(2);
expect(el.querySelector('.js-ci-error')).toEqual(null);
expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path);
expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id);
expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%.`);
});
it('should list single stage', (done) => {
pipeline.details.stages.splice(0, 1);
Vue.nextTick(() => {
expect(el.querySelectorAll('.stage-container button').length).toEqual(1);
expect(el.innerText).toContain('with stage');
done();
});
});
it('should not have stages when there is no stage', (done) => {
vm.mr.pipeline.details.stages = [];
Vue.nextTick(() => {
expect(el.querySelectorAll('.stage-container button').length).toEqual(0);
done();
});
});
it('should not have coverage text when pipeline has no coverage info', (done) => {
vm.mr.pipeline.coverage = null;
Vue.nextTick(() => {
expect(el.querySelector('.js-mr-coverage')).toEqual(null);
done();
});
});
it('should show CI error when there is a CI error', (done) => {
vm.mr.ciStatus = null;
Vue.nextTick(() => {
expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
expect(el.innerText).toContain('Could not connect to the CI server');
done();
});
});
});
});
import Vue from 'vue';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links';
const createComponent = (data) => {
const Component = Vue.extend(relatedLinksComponent);
return new Component({
el: document.createElement('div'),
propsData: data,
});
};
describe('MRWidgetRelatedLinks', () => {
describe('props', () => {
it('should have props', () => {
const { relatedLinks } = relatedLinksComponent.props;
expect(relatedLinks).toBeDefined();
expect(relatedLinks.type instanceof Object).toBeTruthy();
expect(relatedLinks.required).toBeTruthy();
});
});
describe('computed', () => {
describe('hasLinks', () => {
it('should return correct value when we have links reference', () => {
const data = {
relatedLinks: {
closing: '/foo',
mentioned: '/foo',
assignToMe: '/foo',
},
};
const vm = createComponent(data);
expect(vm.hasLinks).toBeTruthy();
vm.relatedLinks.closing = null;
expect(vm.hasLinks).toBeTruthy();
vm.relatedLinks.mentioned = null;
expect(vm.hasLinks).toBeTruthy();
vm.relatedLinks.assignToMe = null;
expect(vm.hasLinks).toBeFalsy();
});
});
});
describe('methods', () => {
const data = {
relatedLinks: {
closing: '<a href="#">#23</a> and <a>#42</a>',
mentioned: '<a href="#">#7</a>',
},
};
const vm = createComponent(data);
describe('hasMultipleIssues', () => {
it('should return true if the given text has multiple issues', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy();
});
it('should return false if the given text has one issue', () => {
expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy();
});
});
describe('issueLabel', () => {
it('should return true if the given text has multiple issues', () => {
expect(vm.issueLabel('closing')).toEqual('issues');
});
it('should return false if the given text has one issue', () => {
expect(vm.issueLabel('mentioned')).toEqual('issue');
});
});
describe('verbLabel', () => {
it('should return true if the given text has multiple issues', () => {
expect(vm.verbLabel('closing')).toEqual('are');
});
it('should return false if the given text has one issue', () => {
expect(vm.verbLabel('mentioned')).toEqual('is');
});
});
});
describe('template', () => {
it('should have only have closing issues text', () => {
const vm = createComponent({
relatedLinks: {
closing: '<a href="#">#23</a> and <a>#42</a>',
},
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issues #23 and #42');
expect(content).not.toContain('mentioned');
});
it('should have only have mentioned issues text', () => {
const vm = createComponent({
relatedLinks: {
mentioned: '<a href="#">#7</a>',
},
});
expect(vm.$el.innerText).toContain('issue #7');
expect(vm.$el.innerText).toContain('is mentioned but will not be closed.');
expect(vm.$el.innerText).not.toContain('Closes');
});
it('should have closing and mentioned issues at the same time', () => {
const vm = createComponent({
relatedLinks: {
closing: '<a href="#">#7</a>',
mentioned: '<a href="#">#23</a> and <a>#42</a>',
},
});
const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(content).toContain('Closes issue #7.');
expect(content).toContain('issues #23 and #42');
expect(content).toContain('are mentioned but will not be closed.');
});
it('should have assing issues link', () => {
const vm = createComponent({
relatedLinks: {
assignToMe: '<a href="#">Assign yourself to these issues</a>',
},
});
expect(vm.$el.innerText).toContain('Assign yourself to these issues');
});
});
});
import Vue from 'vue';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived';
describe('MRWidgetArchived', () => {
describe('template', () => {
it('should have correct elements', () => {
const Component = Vue.extend(archivedComponent);
const el = new Component({
el: document.createElement('div'),
}).$el;
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
expect(el.querySelector('button').disabled).toBeTruthy();
expect(el.innerText).toContain('This project is archived, write access has been disabled.');
});
});
});
import Vue from 'vue';
import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed';
const mergeError = 'This is the merge error';
describe('MRWidgetAutoMergeFailed', () => {
describe('props', () => {
it('should have props', () => {
const mrProp = autoMergeFailedComponent.props.mr;
expect(mrProp.type instanceof Object).toBeTruthy();
expect(mrProp.required).toBeTruthy();
});
});
describe('template', () => {
const Component = Vue.extend(autoMergeFailedComponent);
const vm = new Component({
el: document.createElement('div'),
propsData: {
mr: { mergeError },
},
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically.');
expect(vm.$el.innerText).toContain(mergeError);
});
});
});
import Vue from 'vue';
import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking';
describe('MRWidgetChecking', () => {
describe('template', () => {
it('should have correct elements', () => {
const Component = Vue.extend(checkingComponent);
const el = new Component({
el: document.createElement('div'),
}).$el;
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
expect(el.querySelector('button').disabled).toBeTruthy();
expect(el.innerText).toContain('Checking ability to merge automatically.');
expect(el.querySelector('i')).toBeDefined();
});
});
});
import Vue from 'vue';
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed';
const mr = {
targetBranch: 'good-branch',
targetBranchCommitsPath: '/good-branch',
closedBy: {
name: 'Fatih Acet',
username: 'fatihacet',
},
updatedAt: '2017-03-23T20:08:08.845Z',
closedAt: '1 day ago',
};
const createComponent = () => {
const Component = Vue.extend(closedComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
}).$el;
};
describe('MRWidgetClosed', () => {
describe('props', () => {
it('should have props', () => {
const mrProp = closedComponent.props.mr;
expect(mrProp.type instanceof Object).toBeTruthy();
expect(mrProp.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components added', () => {
expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined();
});
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent();
expect(el.querySelector('h4').textContent).toContain('Closed by');
expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
expect(el.textContent).toContain('The changes were not merged into');
expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchCommitsPath);
expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
});
});
});
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
const path = '/conflicts';
const createComponent = () => {
const Component = Vue.extend(conflictsComponent);
return new Component({
el: document.createElement('div'),
propsData: {
mr: {
canMerge: true,
conflictResolutionPath: path,
},
},
});
};
describe('MRWidgetConflicts', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = conflictsComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
const resolveButton = el.querySelectorAll('.btn-group .btn')[0];
const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1];
expect(el.textContent).toContain('There are merge conflicts.');
expect(el.textContent).not.toContain('ask someone with write access');
expect(el.querySelector('.btn-success').disabled).toBeTruthy();
expect(el.querySelectorAll('.btn-group .btn').length).toBe(2);
expect(resolveButton.textContent).toContain('Resolve conflicts');
expect(resolveButton.getAttribute('href')).toEqual(path);
expect(mergeLocallyButton.textContent).toContain('Merge locally');
});
describe('when user does not have permission to merge', () => {
let vm;
beforeEach(() => {
vm = createComponent();
vm.mr.canMerge = false;
});
it('should show proper message', (done) => {
Vue.nextTick(() => {
expect(vm.$el.textContent).toContain('ask someone with write access');
done();
});
});
it('should not have action buttons', (done) => {
Vue.nextTick(() => {
expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
expect(vm.$el.querySelector('a.js-resolve-conflicts-button')).toEqual(null);
expect(vm.$el.querySelector('a.js-merge-locally-button')).toEqual(null);
done();
});
});
});
});
});
import Vue from 'vue';
import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge';
import eventHub from '~/vue_merge_request_widget/event_hub';
const mr = {
mergeError: 'Merge error happened.',
};
const createComponent = () => {
const Component = Vue.extend(failedToMergeComponent);
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
describe('MRWidgetFailedToMerge', () => {
describe('data', () => {
it('should have default data', () => {
const data = failedToMergeComponent.data();
expect(data.timer).toEqual(10);
expect(data.isRefreshing).toBeFalsy();
});
});
describe('computed', () => {
describe('timerText', () => {
it('should return correct timer text', () => {
const vm = createComponent();
expect(vm.timerText).toEqual('10 seconds');
vm.timer = 1;
expect(vm.timerText).toEqual('a second');
});
});
});
describe('created', () => {
it('should disable polling', () => {
spyOn(eventHub, '$emit');
createComponent();
expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling');
});
});
describe('methods', () => {
describe('refresh', () => {
it('should emit event to request component refresh', () => {
spyOn(eventHub, '$emit');
const vm = createComponent();
expect(vm.isRefreshing).toBeFalsy();
vm.refresh();
expect(vm.isRefreshing).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling');
});
});
describe('updateTimer', () => {
it('should update timer and emit event when timer end', () => {
const vm = createComponent();
spyOn(vm, 'refresh');
expect(vm.timer).toEqual(10);
for (let i = 0; i < 10; i++) { // eslint-disable-line
expect(vm.timer).toEqual(10 - i);
vm.updateTimer();
}
expect(vm.refresh).toHaveBeenCalled();
});
});
});
describe('template', () => {
let vm;
let el;
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
it('should have correct elements', (done) => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('Merge error happened.');
expect(el.innerText).toContain('Refreshing in 10 seconds');
expect(el.innerText).not.toContain('Merge failed.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now');
expect(el.querySelector('.js-refresh-label')).toEqual(null);
expect(el.innerText).not.toContain('Refreshing now...');
setTimeout(() => {
expect(el.innerText).toContain('Refreshing in 9 seconds');
done();
}, 1010);
});
it('should just generic merge failed message if merge_error is not available', (done) => {
vm.mr.mergeError = null;
Vue.nextTick(() => {
expect(el.innerText).toContain('Merge failed.');
expect(el.innerText).not.toContain('Merge error happened.');
done();
});
});
it('should show refresh label when refresh requested', () => {
vm.refresh();
Vue.nextTick(() => {
expect(el.innerText).not.toContain('Merge failed. Refreshing');
expect(el.innerText).toContain('Refreshing now...');
});
});
});
});
import Vue from 'vue';
import lockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_locked';
describe('MRWidgetLocked', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = lockedComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
});
describe('template', () => {
it('should have correct elements', () => {
const Component = Vue.extend(lockedComponent);
const mr = {
targetBranchPath: '/branch-path',
targetBranch: 'branch',
};
const el = new Component({
el: document.createElement('div'),
propsData: { mr },
}).$el;
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('it is locked');
expect(el.innerText).toContain('changes will be merged into');
expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath);
expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch);
});
});
});
import Vue from 'vue';
import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds';
import eventHub from '~/vue_merge_request_widget/event_hub';
const targetBranchPath = '/foo/bar';
const targetBranch = 'foo';
const sha = '1EA2EZ34';
const createComponent = () => {
const Component = Vue.extend(mwpsComponent);
const mr = {
shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true,
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
setToMWPSBy: {},
sha,
targetBranchPath,
targetBranch,
};
const service = {
cancelAutomaticMerge() {},
mergeResource: {
save() {},
},
};
return new Component({
el: document.createElement('div'),
propsData: { mr, service },
});
};
describe('MRWidgetMergeWhenPipelineSucceeds', () => {
describe('props', () => {
it('should have props', () => {
const { mr, service } = mwpsComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
expect(service.type instanceof Object).toBeTruthy();
expect(service.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components added', () => {
expect(mwpsComponent.components['mr-widget-author']).toBeDefined();
});
});
describe('data', () => {
it('should have default data', () => {
const data = mwpsComponent.data();
expect(data.isCancellingAutoMerge).toBeFalsy();
expect(data.isRemovingSourceBranch).toBeFalsy();
});
});
describe('computed', () => {
describe('canRemoveSourceBranch', () => {
it('should return true when user is able to remove source branch', () => {
const vm = createComponent();
expect(vm.canRemoveSourceBranch).toBeTruthy();
});
it('should return false when user id is not the same with who set the MWPS', () => {
const vm = createComponent();
vm.mr.mergeUserId = 2;
expect(vm.canRemoveSourceBranch).toBeFalsy();
vm.mr.currentUserId = 2;
expect(vm.canRemoveSourceBranch).toBeTruthy();
vm.mr.currentUserId = 3;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
it('should return false when shouldRemoveSourceBranch set to false', () => {
const vm = createComponent();
vm.mr.shouldRemoveSourceBranch = true;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
it('should return false if user is not able to remove the source branch', () => {
const vm = createComponent();
vm.mr.canRemoveSourceBranch = false;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
});
});
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
it('should set flag and call service then tell main component to update the widget with data', (done) => {
const vm = createComponent();
const mrObj = {
is_new_mr_data: true,
};
spyOn(eventHub, '$emit');
spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => {
resolve({
json() {
return mrObj;
},
});
}));
vm.cancelAutomaticMerge();
setTimeout(() => {
expect(vm.isCancellingAutoMerge).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
done();
}, 333);
});
});
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
const vm = createComponent();
spyOn(eventHub, '$emit');
spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => {
resolve({
json() {
return {
status: 'merge_when_pipeline_succeeds',
};
},
});
}));
vm.removeSourceBranch();
setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.mergeResource.save).toHaveBeenCalledWith({
sha,
merge_when_pipeline_succeeds: true,
should_remove_source_branch: true,
});
done();
}, 333);
});
});
});
describe('template', () => {
let vm;
let el;
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds.');
expect(el.innerText).toContain('The changes will be merged into');
expect(el.innerText).toContain(targetBranch);
expect(el.innerText).toContain('The source branch will not be removed.');
expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge');
expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch');
expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy();
});
it('should disable cancel auto merge button when the action is in progress', (done) => {
vm.isCancellingAutoMerge = true;
Vue.nextTick(() => {
expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy();
done();
});
});
it('should show source branch will be removed text when it source branch set to remove', (done) => {
vm.mr.shouldRemoveSourceBranch = true;
Vue.nextTick(() => {
const normalizedText = el.innerText.replace(/\s+/g, ' ');
expect(normalizedText).toContain('The source branch will be removed.');
expect(normalizedText).not.toContain('The source branch will not be removed.');
done();
});
});
it('should not show remove source branch button when user not able to remove source branch', (done) => {
vm.mr.currentUserId = 4;
Vue.nextTick(() => {
expect(el.querySelector('.js-remove-source-branch')).toEqual(null);
done();
});
});
it('should disable remove source branch button when the action is in progress', (done) => {
vm.isRemovingSourceBranch = true;
Vue.nextTick(() => {
expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy();
done();
});
});
});
});
import Vue from 'vue';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged';
import eventHub from '~/vue_merge_request_widget/event_hub';
const targetBranch = 'foo';
const createComponent = () => {
const Component = Vue.extend(mergedComponent);
const mr = {
isRemovingSourceBranch: false,
cherryPickInForkPath: false,
canCherryPickInCurrentMR: true,
revertInForkPath: false,
canRevertInCurrentMR: true,
canRemoveSourceBranch: true,
sourceBranchRemoved: true,
mergedBy: {},
mergedAt: '',
updatedAt: '',
targetBranch,
};
const service = {
removeSourceBranch() {},
};
return new Component({
el: document.createElement('div'),
propsData: { mr, service },
});
};
describe('MRWidgetMerged', () => {
describe('props', () => {
it('should have props', () => {
const { mr, service } = mergedComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
expect(service.type instanceof Object).toBeTruthy();
expect(service.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components added', () => {
expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined();
});
});
describe('data', () => {
it('should have default data', () => {
const data = mergedComponent.data();
expect(data.isMakingRequest).toBeFalsy();
});
});
describe('computed', () => {
describe('shouldShowRemoveSourceBranch', () => {
it('should correct value when fields changed', () => {
const vm = createComponent();
vm.mr.sourceBranchRemoved = false;
expect(vm.shouldShowRemoveSourceBranch).toBeTruthy();
vm.mr.sourceBranchRemoved = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
vm.mr.sourceBranchRemoved = false;
vm.mr.canRemoveSourceBranch = false;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
vm.mr.isRemovingSourceBranch = true;
vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
});
});
describe('shouldShowSourceBranchRemoving', () => {
it('should correct value when fields changed', () => {
const vm = createComponent();
vm.mr.sourceBranchRemoved = false;
expect(vm.shouldShowSourceBranchRemoving).toBeFalsy();
vm.mr.sourceBranchRemoved = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
vm.mr.sourceBranchRemoved = false;
vm.isMakingRequest = true;
expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
vm.isMakingRequest = false;
vm.mr.isRemovingSourceBranch = true;
expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
});
});
});
describe('methods', () => {
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
const vm = createComponent();
spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => {
resolve({
json() {
return {
message: 'Branch was removed',
};
},
});
}));
vm.removeSourceBranch();
setTimeout(() => {
const args = eventHub.$emit.calls.argsFor(0);
expect(vm.isMakingRequest).toBeTruthy();
expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).not.toThrow();
done();
}, 333);
});
});
});
describe('template', () => {
let vm;
let el;
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('.js-mr-widget-author')).toBeDefined();
expect(el.innerText).toContain('The changes were merged into');
expect(el.innerText).toContain(targetBranch);
expect(el.innerText).toContain('The source branch has been removed.');
expect(el.innerText).toContain('Revert');
expect(el.innerText).toContain('Cherry-pick');
expect(el.innerText).not.toContain('You can remove source branch now.');
expect(el.innerText).not.toContain('The source branch is being removed.');
});
it('should not show source branch removed text', (done) => {
vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => {
expect(el.innerText).toContain('You can remove source branch now.');
expect(el.innerText).not.toContain('The source branch has been removed.');
done();
});
});
it('should show source branch removing text', (done) => {
vm.mr.isRemovingSourceBranch = true;
vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => {
expect(el.innerText).toContain('The source branch is being removed.');
expect(el.innerText).not.toContain('You can remove source branch now.');
expect(el.innerText).not.toContain('The source branch has been removed.');
done();
});
});
});
});
import Vue from 'vue';
import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch';
const createComponent = () => {
const Component = Vue.extend(missingBranchComponent);
const mr = {
sourceBranchRemoved: true,
};
return new Component({
el: document.createElement('div'),
propsData: { mr },
});
};
describe('MRWidgetMissingBranch', () => {
describe('props', () => {
it('should have props', () => {
const mrProp = missingBranchComponent.props.mr;
expect(mrProp.type instanceof Object).toBeTruthy();
expect(mrProp.required).toBeTruthy();
});
});
describe('components', () => {
it('should have components added', () => {
expect(missingBranchComponent.components['mr-widget-merge-help']).toBeDefined();
});
});
describe('computed', () => {
describe('missingBranchName', () => {
it('should return proper branch name', () => {
const vm = createComponent();
expect(vm.missingBranchName).toEqual('source');
vm.mr.sourceBranchRemoved = false;
expect(vm.missingBranchName).toEqual('target');
});
});
});
describe('template', () => {
it('should have correct elements', () => {
const el = createComponent().$el;
const content = el.textContent.replace(/\n(\s)+/g, ' ').trim();
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(content).toContain('source branch does not exist.');
expect(content).toContain('Please restore the source branch or use a different source branch.');
});
});
});
import Vue from 'vue';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed';
describe('MRWidgetNotAllowed', () => {
describe('template', () => {
const Component = Vue.extend(notAllowedComponent);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request.');
});
});
});
import Vue from 'vue';
import nothingToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge';
describe('MRWidgetNothingToMerge', () => {
describe('template', () => {
const Component = Vue.extend(nothingToMergeComponent);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('There is nothing to merge from source branch into target branch.');
expect(vm.$el.innerText).toContain('Please push new commits or use a different branch.');
});
});
});
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked';
describe('MRWidgetPipelineBlocked', () => {
describe('template', () => {
const Component = Vue.extend(pipelineBlockedComponent);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.');
});
});
});
import Vue from 'vue';
import pipelineFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_failed';
describe('MRWidgetPipelineFailed', () => {
describe('template', () => {
const Component = Vue.extend(pipelineFailedComponent);
const vm = new Component({
el: document.createElement('div'),
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.');
});
});
});
import Vue from 'vue';
import readyToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
import eventHub from '~/vue_merge_request_widget/event_hub';
import * as simplePoll from '~/lib/utils/simple_poll';
const commitMessage = 'This is the commit message';
const commitMessageWithDescription = 'This is the commit message description';
const createComponent = () => {
const Component = Vue.extend(readyToMergeComponent);
const mr = {
isPipelineActive: false,
pipeline: null,
isPipelineFailed: false,
onlyAllowMergeIfPipelineSucceeds: false,
hasCI: false,
ciStatus: null,
sha: '12345678',
commitMessage,
commitMessageWithDescription,
};
const service = {
merge() {},
poll() {},
};
return new Component({
el: document.createElement('div'),
propsData: { mr, service },
});
};
describe('MRWidgetReadyToMerge', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
describe('props', () => {
it('should have props', () => {
const { mr, service } = readyToMergeComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
expect(service.type instanceof Object).toBeTruthy();
expect(service.required).toBeTruthy();
});
});
describe('data', () => {
it('should have default data', () => {
expect(vm.removeSourceBranch).toBeTruthy(true);
expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
expect(vm.showCommitMessageEditor).toBeFalsy();
expect(vm.isMakingRequest).toBeFalsy();
expect(vm.isMergingImmediately).toBeFalsy();
expect(vm.commitMessage).toBe(vm.mr.commitMessage);
expect(vm.successSvg).toBeDefined();
expect(vm.warningSvg).toBeDefined();
});
});
describe('computed', () => {
describe('commitMessageLinkTitle', () => {
const withDesc = 'Include description in commit message';
const withoutDesc = "Don't include description in commit message";
it('should return message wit description', () => {
expect(vm.commitMessageLinkTitle).toEqual(withDesc);
});
it('should return message without description', () => {
vm.useCommitMessageWithDescription = true;
expect(vm.commitMessageLinkTitle).toEqual(withoutDesc);
});
});
describe('mergeButtonClass', () => {
const defaultClass = 'btn btn-success accept-merge-request';
const failedClass = `${defaultClass} btn-danger`;
const inActionClass = `${defaultClass} btn-info`;
it('should return default class', () => {
vm.mr.pipeline = true;
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
it('should return failed class when MR has CI but also has an unknown status', () => {
vm.mr.hasCI = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
});
it('should return default class when MR has no pipeline', () => {
expect(vm.mergeButtonClass).toEqual(defaultClass);
});
it('should return in action class when pipeline is active', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineActive = true;
expect(vm.mergeButtonClass).toEqual(inActionClass);
});
it('should return failed class when pipeline is failed', () => {
vm.mr.pipeline = {};
vm.mr.isPipelineFailed = true;
expect(vm.mergeButtonClass).toEqual(failedClass);
});
});
describe('mergeButtonText', () => {
it('should return Merge', () => {
expect(vm.mergeButtonText).toEqual('Merge');
});
it('should return Merge in progress', () => {
vm.isMergingImmediately = true;
expect(vm.mergeButtonText).toEqual('Merge in progress');
});
it('should return Merge when pipeline succeeds', () => {
vm.isMergingImmediately = false;
vm.mr.isPipelineActive = true;
expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds');
});
});
describe('shouldShowMergeOptionsDropdown', () => {
it('should return false with initial data', () => {
expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy();
});
it('should return true when pipeline active', () => {
vm.mr.isPipelineActive = true;
expect(vm.shouldShowMergeOptionsDropdown).toBeTruthy();
});
it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => {
vm.mr.isPipelineActive = true;
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
expect(vm.shouldShowMergeOptionsDropdown).toBeFalsy();
});
});
describe('isMergeButtonDisabled', () => {
it('should return false with initial data', () => {
expect(vm.isMergeButtonDisabled).toBeFalsy();
});
it('should return true when there is no commit message', () => {
vm.commitMessage = '';
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
it('should return true if merge is not allowed', () => {
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
vm.mr.isPipelineFailed = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
it('should return true when there vm instance is making request', () => {
vm.isMakingRequest = true;
expect(vm.isMergeButtonDisabled).toBeTruthy();
});
});
});
describe('methods', () => {
describe('isMergeAllowed', () => {
it('should return false with initial data', () => {
expect(vm.isMergeAllowed()).toBeTruthy();
});
it('should return false when MR is set only merge when pipeline succeeds', () => {
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
expect(vm.isMergeAllowed()).toBeTruthy();
});
it('should return true true', () => {
vm.mr.onlyAllowMergeIfPipelineSucceeds = true;
vm.mr.isPipelineFailed = true;
expect(vm.isMergeAllowed()).toBeFalsy();
});
});
describe('updateCommitMessage', () => {
it('should revert flag and change commitMessage', () => {
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage);
vm.updateCommitMessage();
expect(vm.useCommitMessageWithDescription).toBeTruthy();
expect(vm.commitMessage).toEqual(commitMessageWithDescription);
vm.updateCommitMessage();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
expect(vm.commitMessage).toEqual(commitMessage);
});
});
describe('toggleCommitMessageEditor', () => {
it('should toggle showCommitMessageEditor flag', () => {
expect(vm.showCommitMessageEditor).toBeFalsy();
vm.toggleCommitMessageEditor();
expect(vm.showCommitMessageEditor).toBeTruthy();
});
});
describe('handleMergeButtonClick', () => {
const returnPromise = status => new Promise((resolve) => {
resolve({
json() {
return { status };
},
});
});
it('should handle merge when pipeline succeeds', (done) => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'merge').and.returnValue(returnPromise('merge_when_pipeline_succeeds'));
vm.removeSourceBranch = false;
vm.handleMergeButtonClick(true);
setTimeout(() => {
expect(vm.setToMergeWhenPipelineSucceeds).toBeTruthy();
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
const params = vm.service.merge.calls.argsFor(0)[0];
expect(params.sha).toEqual(vm.mr.sha);
expect(params.commit_message).toEqual(vm.mr.commitMessage);
expect(params.should_remove_source_branch).toBeFalsy();
expect(params.merge_when_pipeline_succeeds).toBeTruthy();
done();
}, 333);
});
it('should handle merge failed', (done) => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'merge').and.returnValue(returnPromise('failed'));
vm.handleMergeButtonClick(false, true);
setTimeout(() => {
expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
const params = vm.service.merge.calls.argsFor(0)[0];
expect(params.should_remove_source_branch).toBeTruthy();
expect(params.merge_when_pipeline_succeeds).toBeFalsy();
done();
}, 333);
});
it('should handle merge action accepted case', (done) => {
spyOn(vm.service, 'merge').and.returnValue(returnPromise('success'));
spyOn(vm, 'initiateMergePolling');
vm.handleMergeButtonClick();
setTimeout(() => {
expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
expect(vm.isMakingRequest).toBeTruthy();
expect(vm.initiateMergePolling).toHaveBeenCalled();
const params = vm.service.merge.calls.argsFor(0)[0];
expect(params.should_remove_source_branch).toBeTruthy();
expect(params.merge_when_pipeline_succeeds).toBeFalsy();
done();
}, 333);
});
});
describe('initiateMergePolling', () => {
it('should call simplePoll', () => {
spyOn(simplePoll, 'default');
vm.initiateMergePolling();
expect(simplePoll.default).toHaveBeenCalled();
});
});
describe('handleMergePolling', () => {
const returnPromise = state => new Promise((resolve) => {
resolve({
json() {
return { state, source_branch_exists: true };
},
});
});
it('should call start and stop polling when MR merged', (done) => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'poll').and.returnValue(returnPromise('merged'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
vm.handleMergePolling(() => { cpc = true; }, () => { spc = true; });
setTimeout(() => {
expect(vm.service.poll).toHaveBeenCalled();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent');
expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled();
expect(cpc).toBeFalsy();
expect(spc).toBeTruthy();
done();
}, 333);
});
it('should continue polling until MR is merged', (done) => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise('some_other_state'));
spyOn(vm, 'initiateRemoveSourceBranchPolling');
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
vm.handleMergePolling(() => { cpc = true; }, () => { spc = true; });
setTimeout(() => {
expect(cpc).toBeTruthy();
expect(spc).toBeFalsy();
done();
}, 333);
});
});
describe('initiateRemoveSourceBranchPolling', () => {
it('should emit event and call simplePoll', () => {
spyOn(eventHub, '$emit');
spyOn(simplePoll, 'default');
vm.initiateRemoveSourceBranchPolling();
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
expect(simplePoll.default).toHaveBeenCalled();
});
});
describe('handleRemoveBranchPolling', () => {
const returnPromise = state => new Promise((resolve) => {
resolve({
json() {
return { source_branch_exists: state };
},
});
});
it('should call start and stop polling when MR merged', (done) => {
spyOn(eventHub, '$emit');
spyOn(vm.service, 'poll').and.returnValue(returnPromise(false));
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
vm.handleRemoveBranchPolling(() => { cpc = true; }, () => { spc = true; });
setTimeout(() => {
expect(vm.service.poll).toHaveBeenCalled();
const args = eventHub.$emit.calls.argsFor(0);
expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).toBeDefined();
args[1]();
expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
expect(cpc).toBeFalsy();
expect(spc).toBeTruthy();
done();
}, 333);
});
it('should continue polling until MR is merged', (done) => {
spyOn(vm.service, 'poll').and.returnValue(returnPromise(true));
let cpc = false; // continuePollingCalled
let spc = false; // stopPollingCalled
vm.handleRemoveBranchPolling(() => { cpc = true; }, () => { spc = true; });
setTimeout(() => {
expect(cpc).toBeTruthy();
expect(spc).toBeFalsy();
done();
}, 333);
});
});
});
});
import Vue from 'vue';
import unresolvedDiscussionsComponent from '~/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions';
describe('MRWidgetUnresolvedDiscussions', () => {
describe('props', () => {
it('should have props', () => {
const { mr } = unresolvedDiscussionsComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
});
});
describe('template', () => {
let el;
let vm;
const path = 'foo/bar';
beforeEach(() => {
const Component = Vue.extend(unresolvedDiscussionsComponent);
const mr = {
createIssueToResolveDiscussionsPath: path,
};
vm = new Component({
el: document.createElement('div'),
propsData: { mr },
});
el = vm.$el;
});
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('There are unresolved discussions. Please resolve these discussions');
expect(el.innerText).toContain('Create an issue to resolve them later');
expect(el.querySelector('.js-create-issue').getAttribute('href')).toEqual(path);
});
it('should not show create issue button if user cannot create issue', (done) => {
vm.mr.createIssueToResolveDiscussionsPath = '';
Vue.nextTick(() => {
expect(el.querySelector('.js-create-issue')).toEqual(null);
done();
});
});
});
});
import Vue from 'vue';
import wipComponent from '~/vue_merge_request_widget/components/states/mr_widget_wip';
import eventHub from '~/vue_merge_request_widget/event_hub';
const createComponent = () => {
const Component = Vue.extend(wipComponent);
const mr = {
title: 'The best MR ever',
removeWIPPath: '/path/to/remove/wip',
};
const service = {
removeWIP() {},
};
return new Component({
el: document.createElement('div'),
propsData: { mr, service },
});
};
describe('MRWidgetWIP', () => {
describe('props', () => {
it('should have props', () => {
const { mr, service } = wipComponent.props;
expect(mr.type instanceof Object).toBeTruthy();
expect(mr.required).toBeTruthy();
expect(service.type instanceof Object).toBeTruthy();
expect(service.required).toBeTruthy();
});
});
describe('data', () => {
it('should have default data', () => {
const vm = createComponent();
expect(vm.isMakingRequest).toBeFalsy();
});
});
describe('methods', () => {
const mrObj = {
is_new_mr_data: true,
};
describe('removeWIP', () => {
it('should make a request to service and handle response', (done) => {
const vm = createComponent();
spyOn(window, 'Flash').and.returnValue(true);
spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeWIP').and.returnValue(new Promise((resolve) => {
resolve({
json() {
return mrObj;
},
});
}));
vm.removeWIP();
setTimeout(() => {
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
expect(window.Flash).toHaveBeenCalledWith('The merge request can now be merged.', 'notice');
done();
}, 333);
});
});
});
describe('template', () => {
let vm;
let el;
beforeEach(() => {
vm = createComponent();
el = vm.$el;
});
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
expect(el.innerText).toContain('This merge request is currently Work In Progress and therefore unable to merge');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-remove-wip').innerText).toContain('Resolve WIP status');
});
it('should not show removeWIP button is user cannot update MR', (done) => {
vm.mr.removeWIPPath = '';
Vue.nextTick(() => {
expect(el.querySelector('.js-remove-wip')).toEqual(null);
done();
});
});
});
});
/* eslint-disable */
export default {
"id": 132,
"iid": 22,
"assignee_id": null,
"author_id": 1,
"description": "",
"lock_version": null,
"milestone_id": null,
"position": 0,
"state": "merged",
"title": "Update README.md",
"updated_by_id": null,
"created_at": "2017-04-07T12:27:26.718Z",
"updated_at": "2017-04-07T15:39:25.852Z",
"deleted_at": null,
"time_estimate": 0,
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null,
"in_progress_merge_commit_sha": null,
"locked_at": null,
"merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775",
"merge_error": null,
"merge_params": {
"force_remove_source_branch": null
},
"merge_status": "can_be_merged",
"merge_user_id": null,
"merge_when_pipeline_succeeds": false,
"source_branch": "daaaa",
"source_project_id": 19,
"target_branch": "master",
"target_project_id": 19,
"merge_event": {
"author": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"updated_at": "2017-04-07T15:39:25.696Z"
},
"closed_event": null,
"author": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"merge_user": null,
"diff_head_sha": "104096c51715e12e7ae41f9333e9fa35b73f385d",
"diff_head_commit_short_id": "104096c5",
"merge_commit_message": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
"pipeline": {
"id": 172,
"user": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"active": false,
"coverage": "92.16",
"path": "/root/acets-app/pipelines/172",
"details": {
"status": {
"icon": "icon_status_success",
"favicon": "favicon_status_success",
"text": "passed",
"label": "passed",
"group": "success",
"has_details": true,
"details_path": "/root/acets-app/pipelines/172"
},
"duration": null,
"finished_at": "2017-04-07T14:00:14.256Z",
"stages": [
{
"name": "build",
"title": "build: failed",
"status": {
"icon": "icon_status_failed",
"favicon": "favicon_status_failed",
"text": "failed",
"label": "failed",
"group": "failed",
"has_details": true,
"details_path": "/root/acets-app/pipelines/172#build"
},
"path": "/root/acets-app/pipelines/172#build",
"dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=build"
},
{
"name": "review",
"title": "review: skipped",
"status": {
"icon": "icon_status_skipped",
"favicon": "favicon_status_skipped",
"text": "skipped",
"label": "skipped",
"group": "skipped",
"has_details": true,
"details_path": "/root/acets-app/pipelines/172#review"
},
"path": "/root/acets-app/pipelines/172#review",
"dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=review"
}
],
"artifacts": [
],
"manual_actions": [
{
"name": "stop_review",
"path": "/root/acets-app/builds/1427/play",
"playable": false
}
]
},
"flags": {
"latest": false,
"triggered": false,
"stuck": false,
"yaml_errors": false,
"retryable": true,
"cancelable": false
},
"ref": {
"name": "daaaa",
"path": "/root/acets-app/tree/daaaa",
"tag": false,
"branch": true
},
"commit": {
"id": "104096c51715e12e7ae41f9333e9fa35b73f385d",
"short_id": "104096c5",
"title": "Update README.md",
"created_at": "2017-04-07T15:27:18.000+03:00",
"parent_ids": [
"2396536178668d8930c29d904e53bd4d06228b32"
],
"message": "Update README.md",
"author_name": "Administrator",
"author_email": "admin@example.com",
"authored_date": "2017-04-07T15:27:18.000+03:00",
"committer_name": "Administrator",
"committer_email": "admin@example.com",
"committed_date": "2017-04-07T15:27:18.000+03:00",
"author": {
"name": "Administrator",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
"commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
},
"retry_path": "/root/acets-app/pipelines/172/retry",
"created_at": "2017-04-07T12:27:19.520Z",
"updated_at": "2017-04-07T15:28:44.800Z"
},
"work_in_progress": false,
"source_branch_exists": false,
"mergeable_discussions_state": true,
"conflicts_can_be_resolved_in_ui": false,
"branch_missing": true,
"commits_count": 1,
"has_conflicts": false,
"can_be_merged": true,
"has_ci": true,
"ci_status": "success",
"pipeline_status_path": "/root/acets-app/merge_requests/22/pipeline_status",
"issues_links": {
"closing": "",
"mentioned_but_not_closing": ""
},
"current_user": {
"can_resolve_conflicts": true,
"can_remove_source_branch": false,
"can_revert_on_current_merge_request": true,
"can_cherry_pick_on_current_merge_request": true
},
"target_branch_path": "/root/acets-app/branches/master",
"source_branch_path": "/root/acets-app/branches/daaaa",
"conflict_resolution_ui_path": "/root/acets-app/merge_requests/22/conflicts",
"remove_wip_path": "/root/acets-app/merge_requests/22/remove_wip",
"cancel_merge_when_pipeline_succeeds_path": "/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds",
"create_issue_to_resolve_discussions_path": "/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22",
"merge_path": "/root/acets-app/merge_requests/22/merge",
"cherry_pick_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
"revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
"email_patches_path": "/root/acets-app/merge_requests/22.patch",
"plain_diff_path": "/root/acets-app/merge_requests/22.diff",
"ci_status_path": "/root/acets-app/merge_requests/22/ci_status",
"status_path": "/root/acets-app/merge_requests/22.json",
"merge_check_path": "/root/acets-app/merge_requests/22/merge_check",
"ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status",
"project_archived": false,
"merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
"diverged_commits_count": 0,
"only_allow_merge_if_pipeline_succeeds": false,
"commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content"
}
import Vue from 'vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
import mockData from './mock_data';
const createComponent = () => {
delete mrWidgetOptions.el; // Prevent component mounting
gl.mrWidgetData = mockData;
const Component = Vue.extend(mrWidgetOptions);
return new Component();
};
const returnPromise = data => new Promise((resolve) => {
resolve({
json() {
return data;
},
body: data,
});
});
describe('mrWidgetOptions', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
describe('data', () => {
it('should instantiate Store and Service', () => {
expect(vm.mr).toBeDefined();
expect(vm.service).toBeDefined();
});
});
describe('computed', () => {
describe('componentName', () => {
it('should return merged component', () => {
expect(vm.componentName).toEqual('mr-widget-merged');
});
it('should return conflicts component', () => {
vm.mr.state = 'conflicts';
expect(vm.componentName).toEqual('mr-widget-conflicts');
});
});
describe('shouldRenderMergeHelp', () => {
it('should return false for the initial merged state', () => {
expect(vm.shouldRenderMergeHelp).toBeFalsy();
});
it('should return true for a state which requires help widget', () => {
vm.mr.state = 'conflicts';
expect(vm.shouldRenderMergeHelp).toBeTruthy();
});
});
describe('shouldRenderPipelines', () => {
it('should return true for the initial data', () => {
expect(vm.shouldRenderPipelines).toBeTruthy();
});
it('should return true when pipeline is empty but MR.hasCI is set to true', () => {
vm.mr.pipeline = {};
expect(vm.shouldRenderPipelines).toBeTruthy();
});
it('should return true when pipeline available', () => {
vm.mr.hasCI = false;
expect(vm.shouldRenderPipelines).toBeTruthy();
});
it('should return false when there is no pipeline', () => {
vm.mr.pipeline = {};
vm.mr.hasCI = false;
expect(vm.shouldRenderPipelines).toBeFalsy();
});
});
describe('shouldRenderRelatedLinks', () => {
it('should return false for the initial data', () => {
expect(vm.shouldRenderRelatedLinks).toBeFalsy();
});
it('should return true if there is relatedLinks in MR', () => {
vm.mr.relatedLinks = {};
expect(vm.shouldRenderRelatedLinks).toBeTruthy();
});
});
describe('shouldRenderDeployments', () => {
it('should return false for the initial data', () => {
expect(vm.shouldRenderDeployments).toBeFalsy();
});
it('should return true if there is deployments', () => {
vm.mr.deployments.push({}, {});
expect(vm.shouldRenderDeployments).toBeTruthy();
});
});
});
describe('methods', () => {
describe('checkStatus', () => {
it('should tell service to check status', (done) => {
spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData));
spyOn(vm.mr, 'setData');
let isCbExecuted = false;
const cb = () => {
isCbExecuted = true;
};
vm.checkStatus(cb);
setTimeout(() => {
expect(vm.service.checkStatus).toHaveBeenCalled();
expect(vm.mr.setData).toHaveBeenCalled();
expect(isCbExecuted).toBeTruthy();
done();
}, 333);
});
});
describe('initPolling', () => {
it('should call SmartInterval', () => {
spyOn(gl, 'SmartInterval').and.returnValue({
resume() {},
stopTimer() {},
});
vm.initPolling();
expect(vm.pollingInterval).toBeDefined();
expect(gl.SmartInterval).toHaveBeenCalled();
});
});
describe('initDeploymentsPolling', () => {
it('should call SmartInterval', () => {
spyOn(gl, 'SmartInterval');
vm.initDeploymentsPolling();
expect(vm.deploymentsInterval).toBeDefined();
expect(gl.SmartInterval).toHaveBeenCalled();
});
});
describe('fetchDeployments', () => {
it('should fetch deployments', (done) => {
spyOn(vm.service, 'fetchDeployments').and.returnValue(returnPromise([{ deployment: 1 }]));
vm.fetchDeployments();
setTimeout(() => {
expect(vm.service.fetchDeployments).toHaveBeenCalled();
expect(vm.mr.deployments.length).toEqual(1);
expect(vm.mr.deployments[0].deployment).toEqual(1);
done();
}, 333);
});
});
describe('fetchActionsContent', () => {
it('should fetch content of Cherry Pick and Revert modals', (done) => {
spyOn(vm.service, 'fetchMergeActionsContent').and.returnValue(returnPromise('hello world'));
vm.fetchActionsContent();
setTimeout(() => {
expect(vm.service.fetchMergeActionsContent).toHaveBeenCalled();
expect(document.body.textContent).toContain('hello world');
done();
}, 333);
});
});
describe('bindEventHubListeners', () => {
it('should bind eventHub listeners', () => {
spyOn(vm, 'checkStatus').and.returnValue(() => {});
spyOn(vm.service, 'checkStatus').and.returnValue(returnPromise(mockData));
spyOn(vm, 'fetchActionsContent');
spyOn(vm.mr, 'setData');
spyOn(vm, 'resumePolling');
spyOn(vm, 'stopPolling');
spyOn(eventHub, '$on');
vm.bindEventHubListeners();
eventHub.$emit('SetBranchRemoveFlag', ['flag']);
expect(vm.mr.isRemovingSourceBranch).toEqual('flag');
eventHub.$emit('FailedToMerge');
expect(vm.mr.state).toEqual('failedToMerge');
eventHub.$emit('UpdateWidgetData', mockData);
expect(vm.mr.setData).toHaveBeenCalledWith(mockData);
eventHub.$emit('EnablePolling');
expect(vm.resumePolling).toHaveBeenCalled();
eventHub.$emit('DisablePolling');
expect(vm.stopPolling).toHaveBeenCalled();
const listenersWithServiceRequest = {
MRWidgetUpdateRequested: true,
FetchActionsContent: true,
};
const allArgs = eventHub.$on.calls.allArgs();
allArgs.forEach((params) => {
const eventName = params[0];
const callback = params[1];
if (listenersWithServiceRequest[eventName]) {
listenersWithServiceRequest[eventName] = callback;
}
});
listenersWithServiceRequest.MRWidgetUpdateRequested();
expect(vm.checkStatus).toHaveBeenCalled();
listenersWithServiceRequest.FetchActionsContent();
expect(vm.fetchActionsContent).toHaveBeenCalled();
});
});
describe('handleMounted', () => {
it('should call required methods to do the initial kick-off', () => {
spyOn(vm, 'checkStatus');
spyOn(vm, 'initDeploymentsPolling');
spyOn(vm, 'setFavicon');
vm.handleMounted();
expect(vm.checkStatus).toHaveBeenCalled();
expect(vm.setFavicon).toHaveBeenCalled();
expect(vm.initDeploymentsPolling).toHaveBeenCalled();
});
});
describe('setFavicon', () => {
it('should call setFavicon method', () => {
spyOn(gl.utils, 'setFavicon');
vm.setFavicon();
expect(gl.utils.setFavicon).toHaveBeenCalledWith(vm.mr.ciStatusFaviconPath);
});
it('should not call setFavicon when there is no ciStatusFaviconPath', () => {
spyOn(gl.utils, 'setFavicon');
vm.mr.ciStatusFaviconPath = null;
vm.setFavicon();
expect(gl.utils.setFavicon).not.toHaveBeenCalled();
});
});
describe('resumePolling', () => {
it('should call stopTimer on pollingInterval', () => {
spyOn(vm.pollingInterval, 'resume');
vm.resumePolling();
expect(vm.pollingInterval.resume).toHaveBeenCalled();
});
});
describe('stopPolling', () => {
it('should call stopTimer on pollingInterval', () => {
spyOn(vm.pollingInterval, 'stopTimer');
vm.stopPolling();
expect(vm.pollingInterval.stopTimer).toHaveBeenCalled();
});
});
describe('createService', () => {
it('should instantiate a Service', () => {
const endpoints = {
mergePath: '/nice/path',
mergeCheckPath: '/nice/path',
cancelAutoMergePath: '/nice/path',
removeWIPPath: '/nice/path',
sourceBranchPath: '/nice/path',
ciEnvironmentsStatusPath: '/nice/path',
statusPath: '/nice/path',
mergeActionsContentPath: '/nice/path',
};
const serviceInstance = vm.createService(endpoints);
const isInstanceOfMRService = serviceInstance instanceof MRWidgetService;
expect(isInstanceOfMRService).toBe(true);
Object.keys(serviceInstance).forEach((key) => {
expect(serviceInstance[key]).toBeDefined();
});
});
});
});
describe('components', () => {
it('should register all components', () => {
const comps = mrWidgetOptions.components;
expect(comps['mr-widget-header']).toBeDefined();
expect(comps['mr-widget-merge-help']).toBeDefined();
expect(comps['mr-widget-pipeline']).toBeDefined();
expect(comps['mr-widget-deployment']).toBeDefined();
expect(comps['mr-widget-related-links']).toBeDefined();
expect(comps['mr-widget-merged']).toBeDefined();
expect(comps['mr-widget-closed']).toBeDefined();
expect(comps['mr-widget-locked']).toBeDefined();
expect(comps['mr-widget-failed-to-merge']).toBeDefined();
expect(comps['mr-widget-wip']).toBeDefined();
expect(comps['mr-widget-archived']).toBeDefined();
expect(comps['mr-widget-conflicts']).toBeDefined();
expect(comps['mr-widget-nothing-to-merge']).toBeDefined();
expect(comps['mr-widget-not-allowed']).toBeDefined();
expect(comps['mr-widget-missing-branch']).toBeDefined();
expect(comps['mr-widget-ready-to-merge']).toBeDefined();
expect(comps['mr-widget-checking']).toBeDefined();
expect(comps['mr-widget-unresolved-discussions']).toBeDefined();
expect(comps['mr-widget-pipeline-blocked']).toBeDefined();
expect(comps['mr-widget-pipeline-failed']).toBeDefined();
expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
});
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
Vue.use(VueResource);
describe('MRWidgetService', () => {
const mr = {
mergePath: './',
mergeCheckPath: './',
cancelAutoMergePath: './',
removeWIPPath: './',
sourceBranchPath: './',
ciEnvironmentsStatusPath: './',
statusPath: './',
mergeActionsContentPath: './',
isServiceStore: true,
};
it('should have store and resources created in constructor', () => {
const service = new MRWidgetService(mr);
expect(service.mergeResource).toBeDefined();
expect(service.mergeCheckResource).toBeDefined();
expect(service.cancelAutoMergeResource).toBeDefined();
expect(service.removeWIPResource).toBeDefined();
expect(service.removeSourceBranchResource).toBeDefined();
expect(service.deploymentsResource).toBeDefined();
expect(service.pollResource).toBeDefined();
expect(service.mergeActionsContentResource).toBeDefined();
});
it('should have methods defined', () => {
const service = new MRWidgetService(mr);
expect(service.merge()).toBeDefined();
expect(service.cancelAutomaticMerge()).toBeDefined();
expect(service.removeWIP()).toBeDefined();
expect(service.removeSourceBranch()).toBeDefined();
expect(service.fetchDeployments()).toBeDefined();
expect(service.poll()).toBeDefined();
expect(service.checkStatus()).toBeDefined();
expect(service.fetchMergeActionsContent()).toBeDefined();
expect(MRWidgetService.stopEnvironment()).toBeDefined();
});
});
import getStateKey from '~/vue_merge_request_widget/stores/get_state_key';
describe('getStateKey', () => {
it('should return proper state name', () => {
const context = {
mergeStatus: 'checked',
mergeWhenPipelineSucceeds: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: false,
isPipelineFailed: false,
hasMergeableDiscussionsState: false,
isPipelineBlocked: false,
canBeMerged: false,
};
const data = {
project_archived: false,
branch_missing: false,
commits_count: 2,
has_conflicts: false,
work_in_progress: false,
};
const bound = getStateKey.bind(context, data);
expect(bound()).toEqual(null);
context.canBeMerged = true;
expect(bound()).toEqual('readyToMerge');
context.isPipelineBlocked = true;
expect(bound()).toEqual('pipelineBlocked');
context.hasMergeableDiscussionsState = true;
expect(bound()).toEqual('unresolvedDiscussions');
context.onlyAllowMergeIfPipelineSucceeds = true;
context.isPipelineFailed = true;
expect(bound()).toEqual('pipelineFailed');
context.canMerge = false;
expect(bound()).toEqual('notAllowedToMerge');
context.mergeWhenPipelineSucceeds = true;
expect(bound()).toEqual('mergeWhenPipelineSucceeds');
data.work_in_progress = true;
expect(bound()).toEqual('workInProgress');
data.has_conflicts = true;
expect(bound()).toEqual('conflicts');
context.mergeStatus = 'unchecked';
expect(bound()).toEqual('checking');
data.commits_count = 0;
expect(bound()).toEqual('nothingToMerge');
data.branch_missing = true;
expect(bound()).toEqual('missingBranch');
data.project_archived = true;
expect(bound()).toEqual('archived');
});
});
......@@ -81,7 +81,11 @@ describe Gitlab::Prometheus, lib: true do
describe '#query' do
let(:prometheus_query) { prometheus_cpu_query('env-slug') }
let(:query_url) { prometheus_query_url(prometheus_query) }
let(:query_url) { prometheus_query_with_time_url(prometheus_query, Time.now.utc) }
around do |example|
Timecop.freeze { example.run }
end
context 'when request returns vector results' do
it 'returns data from the API call' do
......@@ -123,6 +127,20 @@ describe Gitlab::Prometheus, lib: true do
Timecop.freeze { example.run }
end
context 'when non utc time is passed' do
let(:time_stop) { Time.now.in_time_zone("Warsaw") }
let(:time_start) { time_stop - 8.hours }
let(:query_url) { prometheus_query_range_url(prometheus_query, start: time_start.utc.to_f, stop: time_stop.utc.to_f) }
it 'passed dates are properly converted to utc' do
req_stub = stub_prometheus_request(query_url, body: prometheus_values_body('vector'))
subject.query_range(prometheus_query, start: time_start, stop: time_stop)
expect(req_stub).to have_been_requested
end
end
context 'when a start time is passed' do
let(:query_url) { prometheus_query_range_url(prometheus_query, start: 2.hours.ago) }
......
......@@ -49,6 +49,33 @@ describe Deployment, models: true do
end
end
describe '#metrics' do
let(:deployment) { create(:deployment) }
subject { deployment.metrics(1.hour) }
context 'metrics are disabled' do
it { is_expected.to eq({}) }
end
context 'metrics are enabled' do
let(:simple_metrics) do
{
success: true,
metrics: {},
last_update: 42
}
end
before do
allow(deployment.project).to receive_message_chain(:monitoring_service, :metrics)
.with(any_args).and_return(simple_metrics)
end
it { is_expected.to eq(simple_metrics.merge(deployment_time: deployment.created_at.utc.to_i)) }
end
end
describe '#stop_action' do
let(:build) { create(:ci_build) }
......
......@@ -47,15 +47,30 @@ describe PrometheusService, models: true, caching: true do
describe '#metrics' do
let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
subject { service.metrics(environment) }
around do |example|
Timecop.freeze { example.run }
end
context 'with valid data' do
context 'with valid data without time range' do
subject { service.metrics(environment) }
before do
stub_reactive_cache(service, prometheus_data, 'env-slug', nil, nil)
end
it 'returns reactive data' do
is_expected.to eq(prometheus_data)
end
end
context 'with valid data with time range' do
let(:t_start) { 1.hour.ago.utc }
let(:t_end) { Time.now.utc }
subject { service.metrics(environment, timeframe_start: t_start, timeframe_end: t_end) }
before do
stub_reactive_cache(service, prometheus_data, 'env-slug')
stub_reactive_cache(service, prometheus_data, 'env-slug', t_start, t_end)
end
it 'returns reactive data' do
......@@ -72,7 +87,7 @@ describe PrometheusService, models: true, caching: true do
end
subject do
service.calculate_reactive_cache(environment.slug)
service.calculate_reactive_cache(environment.slug, nil, nil)
end
context 'when service is inactive' do
......
require 'spec_helper'
describe MergeRequestPresenter do
let(:resource) { create :merge_request, source_project: project }
let(:project) { create :empty_project }
let(:user) { create(:user) }
describe '#ci_status' do
subject { described_class.new(resource).ci_status }
context 'when no head pipeline' do
it 'return status using CiService' do
ci_service = double(MockCiService)
ci_status = double
allow(resource.source_project)
.to receive(:ci_service)
.and_return(ci_service)
allow(resource).to receive(:head_pipeline).and_return(nil)
expect(ci_service).to receive(:commit_status)
.with(resource.diff_head_sha, resource.source_branch)
.and_return(ci_status)
is_expected.to eq(ci_status)
end
end
context 'when head pipeline present' do
let(:pipeline) { build_stubbed(:ci_pipeline) }
before do
allow(resource).to receive(:head_pipeline).and_return(pipeline)
end
context 'success with warnings' do
before do
allow(pipeline).to receive(:success?) { true }
allow(pipeline).to receive(:has_warnings?) { true }
end
it 'returns "success_with_warnings"' do
is_expected.to eq('success_with_warnings')
end
end
context 'pipeline HAS status AND its not success with warnings' do
before do
allow(pipeline).to receive(:success?) { false }
allow(pipeline).to receive(:has_warnings?) { false }
end
it 'returns pipeline status' do
is_expected.to eq('pending')
end
end
context 'pipeline has NO status AND its not success with warnings' do
before do
allow(pipeline).to receive(:status) { nil }
allow(pipeline).to receive(:success?) { false }
allow(pipeline).to receive(:has_warnings?) { false }
end
it 'returns "preparing"' do
is_expected.to eq('preparing')
end
end
end
end
describe '#conflict_resolution_path' do
let(:project) { create :empty_project }
let(:user) { create :user }
let(:path) { described_class.new(resource, current_user: user).conflict_resolution_path }
context 'when MR cannot be resolved in UI' do
it 'does not return conflict resolution path' do
allow(resource).to receive(:conflicts_can_be_resolved_in_ui?) { true }
allow(resource).to receive(:conflicts_can_be_resolved_by?).with(user) { false }
expect(path).to be_nil
end
end
context 'when conflicts cannot be resolved by user' do
it 'does not return conflict resolution path' do
allow(resource).to receive(:conflicts_can_be_resolved_in_ui?) { false }
allow(resource).to receive(:conflicts_can_be_resolved_by?).with(user) { true }
expect(path).to be_nil
end
end
context 'when able to access conflict resolution UI' do
it 'does return conflict resolution path' do
allow(resource).to receive(:conflicts_can_be_resolved_in_ui?) { true }
allow(resource).to receive(:conflicts_can_be_resolved_by?).with(user) { true }
expect(path)
.to eq("/#{project.full_path}/merge_requests/#{resource.iid}/conflicts")
end
end
end
context 'issues links' do
let(:project) { create(:project, :private, creator: user, namespace: user.namespace) }
let(:issue_a) { create(:issue, project: project) }
let(:issue_b) { create(:issue, project: project) }
let(:resource) do
create(:merge_request,
source_project: project, target_project: project,
description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}")
end
before do
project.team << [user, :developer]
allow(resource.project).to receive(:default_branch)
.and_return(resource.target_branch)
end
describe '#closing_issues_links' do
subject { described_class.new(resource, current_user: user).closing_issues_links }
it 'presents closing issues links' do
is_expected.to match("#{project.full_path}/issues/#{issue_a.iid}")
end
it 'does not present related issues links' do
is_expected.not_to match("#{project.full_path}/issues/#{issue_b.iid}")
end
end
describe '#mentioned_issues_links' do
subject do
described_class.new(resource, current_user: user)
.mentioned_issues_links
end
it 'presents related issues links' do
is_expected.to match("#{project.full_path}/issues/#{issue_b.iid}")
end
it 'does not present closing issues links' do
is_expected.not_to match("#{project.full_path}/issues/#{issue_a.iid}")
end
end
describe '#assign_to_closing_issues_link' do
subject do
described_class.new(resource, current_user: user)
.assign_to_closing_issues_link
end
before do
assign_issues_service = double(MergeRequests::AssignIssuesService, assignable_issues: assignable_issues)
allow(MergeRequests::AssignIssuesService).to receive(:new)
.and_return(assign_issues_service)
end
context 'single closing issue' do
let(:issue) { create(:issue) }
let(:assignable_issues) { [issue] }
it 'returns correct link with correct text' do
is_expected
.to match("#{project.full_path}/merge_requests/#{resource.iid}/assign_related_issues")
is_expected
.to match("Assign yourself to this issue")
end
end
context 'multiple closing issues' do
let(:issues) { create_list(:issue, 2) }
let(:assignable_issues) { issues }
it 'returns correct link with correct text' do
is_expected
.to match("#{project.full_path}/merge_requests/#{resource.iid}/assign_related_issues")
is_expected
.to match("Assign yourself to these issues")
end
end
context 'no closing issue' do
let(:assignable_issues) { [] }
it 'returns correct link with correct text' do
is_expected.to be_nil
end
end
end
end
describe '#cancel_merge_when_pipeline_succeeds_path' do
subject do
described_class.new(resource, current_user: user)
.cancel_merge_when_pipeline_succeeds_path
end
context 'when can cancel mwps' do
it 'returns path' do
allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
.with(user)
.and_return(true)
is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_merge_when_pipeline_succeeds")
end
end
context 'when cannot cancel mwps' do
it 'returns nil' do
allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
.with(user)
.and_return(false)
is_expected.to be_nil
end
end
end
describe '#merge_path' do
subject do
described_class.new(resource, current_user: user).merge_path
end
context 'when can be merged by user' do
it 'returns path' do
allow(resource).to receive(:can_be_merged_by?)
.with(user)
.and_return(true)
is_expected
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/merge")
end
end
context 'when cannot be merged by user' do
it 'returns nil' do
allow(resource).to receive(:can_be_merged_by?)
.with(user)
.and_return(false)
is_expected.to be_nil
end
end
end
describe '#create_issue_to_resolve_discussions_path' do
subject do
described_class.new(resource, current_user: user)
.create_issue_to_resolve_discussions_path
end
context 'when can create issue and issues enabled' do
it 'returns path' do
allow(project).to receive(:issues_enabled?) { true }
project.team << [user, :master]
is_expected
.to eq("/#{resource.project.full_path}/issues/new?merge_request_to_resolve_discussions_of=#{resource.iid}")
end
end
context 'when cannot create issue' do
it 'returns nil' do
allow(project).to receive(:issues_enabled?) { true }
is_expected.to be_nil
end
end
context 'when issues disabled' do
it 'returns nil' do
allow(project).to receive(:issues_enabled?) { false }
project.team << [user, :master]
is_expected.to be_nil
end
end
end
describe '#remove_wip_path' do
subject do
described_class.new(resource, current_user: user).remove_wip_path
end
context 'when merge request enabled and has permission' do
it 'has remove_wip_path' do
allow(project).to receive(:merge_requests_enabled?) { true }
project.team << [user, :master]
is_expected
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip")
end
end
context 'when has no permission' do
it 'returns nil' do
is_expected.to be_nil
end
end
end
describe '#target_branch_commits_path' do
subject do
described_class.new(resource, current_user: user)
.target_branch_commits_path
end
context 'when target branch exists' do
it 'returns path' do
allow(resource).to receive(:target_branch_exists?) { true }
is_expected
.to eq("/#{resource.target_project.full_path}/commits/#{resource.target_branch}")
end
end
context 'when target branch does not exists' do
it 'returns nil' do
allow(resource).to receive(:target_branch_exists?) { false }
is_expected.to be_nil
end
end
end
describe '#source_branch_path' do
subject do
described_class.new(resource, current_user: user).source_branch_path
end
context 'when source branch exists' do
it 'returns path' do
allow(resource).to receive(:source_branch_exists?) { true }
is_expected
.to eq("/#{resource.source_project.full_path}/branches/#{resource.source_branch}")
end
end
context 'when source branch does not exists' do
it 'returns nil' do
allow(resource).to receive(:source_branch_exists?) { false }
is_expected.to be_nil
end
end
end
end
......@@ -6,7 +6,7 @@ describe BuildEntity do
let(:request) { double('request') }
before do
allow(request).to receive(:user).and_return(user)
allow(request).to receive(:current_user).and_return(user)
end
let(:entity) do
......
......@@ -4,7 +4,7 @@ describe BuildSerializer do
let(:user) { create(:user) }
let(:serializer) do
described_class.new(user: user)
described_class.new(current_user: user)
end
subject { serializer.represent(resource) }
......
......@@ -8,7 +8,7 @@ describe DeploymentEntity do
subject { entity.as_json }
before do
allow(request).to receive(:user).and_return(user)
allow(request).to receive(:current_user).and_return(user)
end
it 'exposes internal deployment id' do
......
......@@ -6,7 +6,7 @@ describe EnvironmentSerializer do
let(:json) do
described_class
.new(user: user, project: project)
.new(current_user: user, project: project)
.represent(resource)
end
......
require 'spec_helper'
describe EventEntity do
subject { described_class.represent(create(:event)).as_json }
it 'exposes author' do
expect(subject).to include(:author)
end
it 'exposes core elements of event' do
expect(subject).to include(:updated_at)
end
end
require 'spec_helper'
describe MergeRequestBasicSerializer do
let(:resource) { create(:merge_request) }
let(:user) { create(:user) }
subject { described_class.new.represent(resource) }
it 'has important MergeRequest attributes' do
expect(subject).to include(:merge_status)
end
end
require 'spec_helper'
describe MergeRequestEntity do
let(:project) { create :empty_project }
let(:resource) { create(:merge_request, source_project: project, target_project: project) }
let(:user) { create(:user) }
let(:request) { double('request', current_user: user) }
subject do
described_class.new(resource, request: request).as_json
end
it 'includes author' do
req = double('request')
author_payload = UserEntity
.represent(resource.author, request: req)
.as_json
expect(subject[:author]).to eq(author_payload)
end
it 'includes pipeline' do
req = double('request', current_user: user)
pipeline = build_stubbed(:ci_pipeline)
allow(resource).to receive(:head_pipeline).and_return(pipeline)
pipeline_payload = PipelineEntity
.represent(pipeline, request: req)
.as_json
expect(subject[:pipeline]).to eq(pipeline_payload)
end
it 'includes issues_links' do
issues_links = subject[:issues_links]
expect(issues_links).to include(:closing, :mentioned_but_not_closing,
:assign_to_closing)
end
it 'has important MergeRequest attributes' do
expect(subject).to include(:diff_head_sha, :merge_commit_message,
:has_conflicts, :has_ci, :merge_path,
:conflict_resolution_path,
:cancel_merge_when_pipeline_succeeds_path,
:create_issue_to_resolve_discussions_path,
:source_branch_path, :target_branch_commits_path,
:commits_count)
end
it 'has email_patches_path' do
expect(subject[:email_patches_path])
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch")
end
it 'has plain_diff_path' do
expect(subject[:plain_diff_path])
.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.diff")
end
it 'has merge_commit_message_with_description' do
expect(subject[:merge_commit_message_with_description])
.to eq(resource.merge_commit_message(include_description: true))
end
describe 'diff_head_sha' do
before do
allow(resource).to receive(:diff_head_sha) { 'sha' }
end
context 'when no diff head commit' do
it 'returns nil' do
allow(resource).to receive(:diff_head_commit) { nil }
expect(subject[:diff_head_sha]).to be_nil
end
end
context 'when diff head commit present' do
it 'returns diff head commit short id' do
allow(resource).to receive(:diff_head_commit) { double }
expect(subject[:diff_head_sha]).to eq('sha')
end
end
end
it 'includes merge_event' do
create(:event, :merged, author: user, project: resource.project, target: resource)
expect(subject[:merge_event]).to include(:author, :updated_at)
end
it 'includes closed_event' do
create(:event, :closed, author: user, project: resource.project, target: resource)
expect(subject[:closed_event]).to include(:author, :updated_at)
end
describe 'diverged_commits_count' do
context 'when MR open and its diverging' do
it 'returns diverged commits count' do
allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: true,
diverged_commits_count: 10)
expect(subject[:diverged_commits_count]).to eq(10)
end
end
context 'when MR is not open' do
it 'returns 0' do
allow(resource).to receive_messages(open?: false)
expect(subject[:diverged_commits_count]).to be_zero
end
end
context 'when MR is not diverging' do
it 'returns 0' do
allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: false)
expect(subject[:diverged_commits_count]).to be_zero
end
end
end
end
require 'spec_helper'
describe MergeRequestSerializer do
let(:user) { build_stubbed(:user) }
let(:merge_request) { build_stubbed(:merge_request) }
let(:serializer) do
described_class.new(current_user: user)
end
describe '#represent' do
let(:opts) { { basic: basic } }
subject { serializer.represent(merge_request, basic: basic) }
context 'when basic param is truthy' do
let(:basic) { true }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
.with(merge_request, opts, MergeRequestBasicEntity)
subject
end
end
context 'when basic param is falsy' do
let(:basic) { false }
it 'calls super class #represent with correct params' do
expect_any_instance_of(BaseSerializer).to receive(:represent)
.with(merge_request, opts, MergeRequestEntity)
subject
end
end
end
end
......@@ -5,7 +5,7 @@ describe PipelineEntity do
let(:request) { double('request') }
before do
allow(request).to receive(:user).and_return(user)
allow(request).to receive(:current_user).and_return(user)
end
let(:entity) do
......@@ -19,7 +19,7 @@ describe PipelineEntity do
let(:pipeline) { create(:ci_empty_pipeline) }
it 'contains required fields' do
expect(subject).to include :id, :user, :path
expect(subject).to include :id, :user, :path, :coverage
expect(subject).to include :ref, :commit
expect(subject).to include :updated_at, :created_at
end
......
......@@ -4,7 +4,7 @@ describe PipelineSerializer do
let(:user) { create(:user) }
let(:serializer) do
described_class.new(user: user)
described_class.new(current_user: user)
end
subject { serializer.represent(resource) }
......@@ -44,7 +44,7 @@ describe PipelineSerializer do
end
let(:serializer) do
described_class.new(user: user)
described_class.new(current_user: user)
.with_pagination(request, response)
end
......@@ -113,7 +113,7 @@ describe PipelineSerializer do
it "verifies number of queries" do
recorded = ActiveRecord::QueryRecorder.new { subject }
expect(recorded.count).to be_within(1).of(50)
expect(recorded.count).to be_within(1).of(58)
expect(recorded.cached_count).to eq(0)
end
......
......@@ -14,7 +14,7 @@ describe StageEntity do
end
before do
allow(request).to receive(:user).and_return(user)
allow(request).to receive(:current_user).and_return(user)
create(:ci_build, :success, pipeline: pipeline)
end
......
......@@ -268,6 +268,24 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
end
context 'when there are only manual actions in stages' do
before do
create_build('image', stage_idx: 0, when: 'manual', allow_failure: true)
create_build('build', stage_idx: 1, when: 'manual', allow_failure: true)
create_build('deploy', stage_idx: 2, when: 'manual')
create_build('check', stage_idx: 3)
process_pipeline
end
it 'processes all jobs until blocking actions encountered' do
expect(all_builds_statuses).to eq(%w[manual manual manual created])
expect(all_builds_names).to eq(%w[image build deploy check])
expect(pipeline.reload).to be_blocked
end
end
context 'when blocking manual actions are defined' do
before do
create_build('code:test', stage_idx: 0)
......@@ -441,6 +459,10 @@ describe Ci::ProcessPipelineService, '#execute', :services do
builds.pluck(:name)
end
def all_builds_names
all_builds.pluck(:name)
end
def builds_statuses
builds.pluck(:status)
end
......
......@@ -7,17 +7,29 @@ module PrometheusHelpers
%{sum(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) / count(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}) * 100}
end
def prometheus_ping_url(prometheus_query)
query = { query: prometheus_query }.to_query
"https://prometheus.example.com/api/v1/query?#{query}"
end
def prometheus_query_url(prometheus_query)
query = { query: prometheus_query }.to_query
"https://prometheus.example.com/api/v1/query?#{query}"
end
def prometheus_query_range_url(prometheus_query, start: 8.hours.ago)
def prometheus_query_with_time_url(prometheus_query, time)
query = { query: prometheus_query, time: time.to_f }.to_query
"https://prometheus.example.com/api/v1/query?#{query}"
end
def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f)
query = {
query: prometheus_query,
start: start.to_f,
end: Time.now.utc.to_f,
end: stop,
step: 1.minute.to_i
}.to_query
......@@ -39,7 +51,12 @@ module PrometheusHelpers
def stub_all_prometheus_requests(environment_slug, body: nil, status: 200)
stub_prometheus_request(
prometheus_query_url(prometheus_memory_query(environment_slug)),
prometheus_query_with_time_url(prometheus_memory_query(environment_slug), Time.now.utc),
status: status,
body: body || prometheus_value_body
)
stub_prometheus_request(
prometheus_query_with_time_url(prometheus_memory_query(environment_slug), 8.hours.ago),
status: status,
body: body || prometheus_value_body
)
......@@ -49,7 +66,12 @@ module PrometheusHelpers
body: body || prometheus_values_body
)
stub_prometheus_request(
prometheus_query_url(prometheus_cpu_query(environment_slug)),
prometheus_query_with_time_url(prometheus_cpu_query(environment_slug), Time.now.utc),
status: status,
body: body || prometheus_value_body
)
stub_prometheus_request(
prometheus_query_with_time_url(prometheus_cpu_query(environment_slug), 8.hours.ago),
status: status,
body: body || prometheus_value_body
)
......@@ -66,8 +88,10 @@ module PrometheusHelpers
metrics: {
memory_values: prometheus_values_body('matrix').dig(:data, :result),
memory_current: prometheus_value_body('vector').dig(:data, :result),
memory_previous: prometheus_value_body('vector').dig(:data, :result),
cpu_values: prometheus_values_body('matrix').dig(:data, :result),
cpu_current: prometheus_value_body('vector').dig(:data, :result)
cpu_current: prometheus_value_body('vector').dig(:data, :result),
cpu_previous: prometheus_value_body('vector').dig(:data, :result)
},
last_update: last_update
}
......
......@@ -328,7 +328,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
context 'only notify for the default branch' do
context 'when enabled' do
let(:pipeline) do
create(:ci_pipeline, project: project, status: 'failed', ref: 'not-the-default-branch')
create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
end
before do
......@@ -342,6 +342,18 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(result).to be_falsy
end
end
context 'when disabled' do
let(:pipeline) do
create(:ci_pipeline, :failed, project: project, ref: 'not-the-default-branch')
end
before do
chat_service.notify_only_default_branch = false
end
it_behaves_like 'call Slack/Mattermost API'
end
end
end
end
require_relative './wait_for_ajax'
require_relative './wait_for_vue_resource'
module WaitForRequests
extend self
include WaitForAjax
include WaitForVueResource
# This is inspired by http://www.salsify.com/blog/engineering/tearing-capybara-ajax-tests
def wait_for_requests_complete
Gitlab::Testing::RequestBlockerMiddleware.block_requests!
wait_for('pending AJAX requests complete') do
Gitlab::Testing::RequestBlockerMiddleware.num_active_requests.zero? &&
finished_all_ajax_requests?
finished_all_requests?
end
ensure
Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
end
def finished_all_requests?
finished_all_ajax_requests? && finished_all_vue_resource_requests?
end
# Waits until the passed block returns true
def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
wait_until = Time.now + max_wait_time.seconds
......
module WaitForVueResource
def wait_for_vue_resource(spinner: true)
Timeout.timeout(Capybara.default_max_wait_time) do
loop until page.evaluate_script('window.activeVueResources').zero?
loop until finished_all_vue_resource_requests?
end
end
private
def finished_all_vue_resource_requests?
return true unless javascript_test?
page.evaluate_script('window.activeVueResources || 0').zero?
end
def javascript_test?
Capybara.current_driver == Capybara.javascript_driver
end
end
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