Commit 07557a94 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge branch 'master' into 'qa-define-selectors'

# Conflicts:
#   qa/qa/page/group/show.rb
parents bf01452c 03f386c2
...@@ -321,6 +321,7 @@ setup-test-env: ...@@ -321,6 +321,7 @@ setup-test-env:
expire_in: 7d expire_in: 7d
paths: paths:
- tmp/tests - tmp/tests
- config/secrets.yml
rspec-pg 0 27: *rspec-metadata-pg rspec-pg 0 27: *rspec-metadata-pg
rspec-pg 1 27: *rspec-metadata-pg rspec-pg 1 27: *rspec-metadata-pg
......
...@@ -2,6 +2,22 @@ ...@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.4.2 (2018-01-30)
### Fixed (6 changes)
- Fix copy/paste on iOS devices due to a bug in webkit. !15804
- Fix missing "allow users to request access" option in public project permissions. !16485
- Fix encoding issue when counting commit count. !16637
- Fixes destination already exists, and some particular service errors on Import/Export error. !16714
- Fix cache clear bug withg using : on Windows. !16740
- Use has_table_privilege for TRIGGER on PostgreSQL.
### Changed (1 change)
- Vendor Auto DevOps template with DAST security checks enabled. !16691
## 10.4.1 (2018-01-24) ## 10.4.1 (2018-01-24)
### Fixed (4 changes) ### Fixed (4 changes)
......
...@@ -325,7 +325,7 @@ group :development, :test do ...@@ -325,7 +325,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2' gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5' gem 'rspec_profiling', '~> 0.0.5'
gem 'rspec-set', '~> 0.1.3' gem 'rspec-set', '~> 0.1.3'
gem 'rspec-parameterized' gem 'rspec-parameterized', require: false
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826) # Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
gem 'minitest', '~> 5.7.0' gem 'minitest', '~> 5.7.0'
......
...@@ -304,7 +304,7 @@ GEM ...@@ -304,7 +304,7 @@ GEM
mime-types (>= 1.16) mime-types (>= 1.16)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.6.3) gitlab-markup (1.6.3)
gitlab-styles (2.3.1) gitlab-styles (2.3.2)
rubocop (~> 0.51) rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0) rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19) rubocop-rspec (~> 1.19)
......
/* global ace */ /* global ace */
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator'; import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob { export default class EditBlob {
...@@ -56,12 +59,14 @@ export default class EditBlob { ...@@ -56,12 +59,14 @@ export default class EditBlob {
if (paneId === '#preview') { if (paneId === '#preview') {
this.$toggleButton.hide(); this.$toggleButton.hide();
return $.post(currentLink.data('preview-url'), { axios.post(currentLink.data('preview-url'), {
content: this.editor.getValue(), content: this.editor.getValue(),
}, (response) => { })
currentPane.empty().append(response); .then(({ data }) => {
return currentPane.renderGFM(); currentPane.empty().append(data);
}); currentPane.renderGFM();
})
.catch(() => createFlash(__('An error occurred previewing the blob')));
} }
this.$toggleButton.show(); this.$toggleButton.show();
......
...@@ -12,6 +12,7 @@ export default class CreateItemDropdown { ...@@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {}); this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData; this.getDataOption = options.getData;
this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown; this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent(); this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer'); this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
...@@ -30,15 +31,15 @@ export default class CreateItemDropdown { ...@@ -30,15 +31,15 @@ export default class CreateItemDropdown {
filterable: true, filterable: true,
remote: false, remote: false,
search: { search: {
fields: ['title'], fields: ['text'],
}, },
selectable: true, selectable: true,
toggleLabel(selected) { toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : this.defaultToggleLabel; return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
}, },
fieldName: this.fieldName, fieldName: this.fieldName,
text(item) { text(item) {
return _.escape(item.title); return _.escape(item.text);
}, },
id(item) { id(item) {
return _.escape(item.id); return _.escape(item.id);
...@@ -51,6 +52,11 @@ export default class CreateItemDropdown { ...@@ -51,6 +52,11 @@ export default class CreateItemDropdown {
}); });
} }
clearDropdown() {
this.$dropdownContainer.find('.dropdown-content').html('');
this.$dropdownContainer.find('.dropdown-input-field').val('');
}
bindEvents() { bindEvents() {
this.$createButton.on('click', this.onClickCreateWildcard.bind(this)); this.$createButton.on('click', this.onClickCreateWildcard.bind(this));
} }
...@@ -58,9 +64,13 @@ export default class CreateItemDropdown { ...@@ -58,9 +64,13 @@ export default class CreateItemDropdown {
onClickCreateWildcard(e) { onClickCreateWildcard(e) {
e.preventDefault(); e.preventDefault();
this.refreshData();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
refreshData() {
// Refresh the dropdown's data, which ends up calling `getData` // Refresh the dropdown's data, which ends up calling `getData`
this.$dropdown.data('glDropdown').remote.execute(); this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
} }
getData(term, callback) { getData(term, callback) {
...@@ -79,20 +89,28 @@ export default class CreateItemDropdown { ...@@ -79,20 +89,28 @@ export default class CreateItemDropdown {
}); });
} }
toggleCreateNewButton(item) { createNewItemFromValue(newValue) {
if (item) { if (this.createNewItemFromValueOption) {
this.selectedItem = { return this.createNewItemFromValueOption(newValue);
title: item, }
id: item,
text: item, return {
}; title: newValue,
id: newValue,
text: newValue,
};
}
toggleCreateNewButton(newValue) {
if (newValue) {
this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer this.$dropdownContainer
.find('.js-dropdown-create-new-item code') .find('.js-dropdown-create-new-item code')
.text(item); .text(newValue);
} }
this.toggleFooter(!item); this.toggleFooter(!newValue);
} }
toggleFooter(toggleState) { toggleFooter(toggleState) {
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global fuzzaldrinPlus */ /* global fuzzaldrinPlus */
import _ from 'underscore'; import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus'; import fuzzaldrinPlus from 'fuzzaldrin-plus';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility'; import { isObject } from './lib/utils/type_utility';
...@@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() { ...@@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() {
}; };
GitLabDropdownRemote.prototype.fetchData = function() { GitLabDropdownRemote.prototype.fetchData = function() {
return $.ajax({ if (this.options.beforeSend) {
url: this.dataEndpoint, this.options.beforeSend();
dataType: this.options.dataType, }
beforeSend: (function(_this) {
return function() { // Fetch the data through ajax if the data is a string
if (_this.options.beforeSend) { return axios.get(this.dataEndpoint)
return _this.options.beforeSend(); .then(({ data }) => {
} if (this.options.success) {
}; return this.options.success(data);
})(this), }
success: (function(_this) { });
return function(data) {
if (_this.options.success) {
return _this.options.success(data);
}
};
})(this)
});
// Fetch the data through ajax if the data is a string
}; };
return GitLabDropdownRemote; return GitLabDropdownRemote;
......
import flash from '../flash';
import { __ } from '../locale';
import axios from '../lib/utils/axios_utils';
import ContributorsStatGraph from './stat_graph_contributors'; import ContributorsStatGraph from './stat_graph_contributors';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
$.ajax({ const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
type: 'GET',
url: document.querySelector('.js-graphs-show').dataset.projectGraphPath, axios.get(url)
dataType: 'json', .then(({ data }) => {
success(data) {
const graph = new ContributorsStatGraph(); const graph = new ContributorsStatGraph();
graph.init(data); graph.init(data);
...@@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
$('.stat-graph').fadeIn(); $('.stat-graph').fadeIn();
$('.loading-graph').hide(); $('.loading-graph').hide();
}, })
}); .catch(() => flash(__('Error fetching contributors data.')));
}); });
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { __ } from './locale';
export default class GroupLabelSubscription { export default class GroupLabelSubscription {
constructor(container) { constructor(container) {
const $container = $(container); const $container = $(container);
...@@ -13,14 +17,12 @@ export default class GroupLabelSubscription { ...@@ -13,14 +17,12 @@ export default class GroupLabelSubscription {
event.preventDefault(); event.preventDefault();
const url = this.$unsubscribeButtons.attr('data-url'); const url = this.$unsubscribeButtons.attr('data-url');
axios.post(url)
$.ajax({ .then(() => {
type: 'POST', this.toggleSubscriptionButtons();
url, this.$unsubscribeButtons.removeAttr('data-url');
}).done(() => { })
this.toggleSubscriptionButtons(); .catch(() => flash(__('There was an error when unsubscribing from this label.')));
this.$unsubscribeButtons.removeAttr('data-url');
});
} }
subscribe(event) { subscribe(event) {
...@@ -31,12 +33,9 @@ export default class GroupLabelSubscription { ...@@ -31,12 +33,9 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url); this.$unsubscribeButtons.attr('data-url', url);
$.ajax({ axios.post(url)
type: 'POST', .then(() => this.toggleSubscriptionButtons())
url, .catch(() => flash(__('There was an error when subscribing to this label.')));
}).done(() => {
this.toggleSubscriptionButtons();
});
} }
toggleSubscriptionButtons() { toggleSubscriptionButtons() {
......
import Flash from '../flash'; import axios from '../lib/utils/axios_utils';
import flash from '../flash';
export default class IntegrationSettingsForm { export default class IntegrationSettingsForm {
constructor(formSelector) { constructor(formSelector) {
...@@ -95,29 +96,26 @@ export default class IntegrationSettingsForm { ...@@ -95,29 +96,26 @@ export default class IntegrationSettingsForm {
*/ */
testSettings(formData) { testSettings(formData) {
this.toggleSubmitBtnState(true); this.toggleSubmitBtnState(true);
$.ajax({
type: 'PUT', return axios.put(this.testEndPoint, formData)
url: this.testEndPoint, .then(({ data }) => {
data: formData, if (data.error) {
}) flash(`${data.message} ${data.service_response}`, 'alert', document, {
.done((res) => { title: 'Save anyway',
if (res.error) { clickHandler: (e) => {
new Flash(`${res.message} ${res.service_response}`, 'alert', document, { e.preventDefault();
title: 'Save anyway', this.$form.submit();
clickHandler: (e) => { },
e.preventDefault(); });
this.$form.submit(); } else {
}, this.$form.submit();
}); }
} else {
this.$form.submit(); this.toggleSubmitBtnState(false);
} })
}) .catch(() => {
.fail(() => { flash('Something went wrong on our end.');
new Flash('Something went wrong on our end.'); this.toggleSubmitBtnState(false);
}) });
.always(() => {
this.toggleSubmitBtnState(false);
});
} }
} }
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */ /* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash'; import Flash from './flash';
export default { export default {
...@@ -22,15 +23,9 @@ export default { ...@@ -22,15 +23,9 @@ export default {
}, },
submit() { submit() {
const _this = this; axios[this.form.attr('method')](this.form.attr('action'), this.getFormDataAsObject())
const xhr = $.ajax({ .then(() => window.location.reload())
url: this.form.attr('action'), .catch(() => this.onFormSubmitFailure());
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => this.onFormSubmitFailure());
}, },
onFormSubmitFailure() { onFormSubmitFailure() {
......
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { __ } from './locale';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar'; import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
...@@ -20,23 +23,24 @@ export default class IssuableIndex { ...@@ -20,23 +23,24 @@ export default class IssuableIndex {
} }
static resetIncomingEmailToken() { static resetIncomingEmailToken() {
$('.incoming-email-token-reset').on('click', (e) => { const $resetToken = $('.incoming-email-token-reset');
$resetToken.on('click', (e) => {
e.preventDefault(); e.preventDefault();
$.ajax({ $resetToken.text('resetting...');
type: 'PUT',
url: $('.incoming-email-token-reset').attr('href'), axios.put($resetToken.attr('href'))
dataType: 'json', .then(({ data }) => {
success(response) { $('#issuable_email').val(data.new_address).focus();
$('#issuable_email').val(response.new_address).focus();
}, $resetToken.text('reset it');
beforeSend() { })
$('.incoming-email-token-reset').text('resetting...'); .catch(() => {
}, flash(__('There was an error when reseting email token.'));
complete() {
$('.incoming-email-token-reset').text('reset it'); $resetToken.text('reset it');
}, });
});
}); });
} }
} }
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
import Sortable from 'vendor/Sortable'; import Sortable from 'vendor/Sortable';
import Flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils';
export default class LabelManager { export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) { constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
...@@ -50,11 +51,12 @@ export default class LabelManager { ...@@ -50,11 +51,12 @@ export default class LabelManager {
if (persistState == null) { if (persistState == null) {
persistState = true; persistState = true;
} }
let xhr;
const _this = this; const _this = this;
const url = $label.find('.js-toggle-priority').data('url'); const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels; let $target = this.prioritizedLabels;
let $from = this.otherLabels; let $from = this.otherLabels;
const rollbackLabelPosition = this.rollbackLabelPosition.bind(this, $label, action);
if (action === 'remove') { if (action === 'remove') {
$target = this.otherLabels; $target = this.otherLabels;
$from = this.prioritizedLabels; $from = this.prioritizedLabels;
...@@ -71,40 +73,34 @@ export default class LabelManager { ...@@ -71,40 +73,34 @@ export default class LabelManager {
return; return;
} }
if (action === 'remove') { if (action === 'remove') {
xhr = $.ajax({ axios.delete(url)
url, .catch(rollbackLabelPosition);
type: 'DELETE'
});
// Restore empty message // Restore empty message
if (!$from.find('li').length) { if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden'); $from.find('.empty-message').removeClass('hidden');
} }
} else { } else {
xhr = this.savePrioritySort($label, action); this.savePrioritySort($label, action)
.catch(rollbackLabelPosition);
} }
return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
} }
onPrioritySortUpdate() { onPrioritySortUpdate() {
const xhr = this.savePrioritySort(); this.savePrioritySort()
return xhr.fail(function() { .catch(() => flash(this.errorMessage));
return new Flash(this.errorMessage, 'alert');
});
} }
savePrioritySort() { savePrioritySort() {
return $.post({ return axios.post(this.prioritizedLabels.data('url'), {
url: this.prioritizedLabels.data('url'), label_ids: this.getSortedLabelsIds(),
data: {
label_ids: this.getSortedLabelsIds()
}
}); });
} }
rollbackLabelPosition($label, originalAction) { rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove'; const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false); this.toggleLabelPriority($label, action, false);
return new Flash(this.errorMessage, 'alert'); flash(this.errorMessage);
} }
getSortedLabelsIds() { getSortedLabelsIds() {
......
...@@ -76,7 +76,13 @@ ...@@ -76,7 +76,13 @@
.then(data => this.store.storeDeploymentData(data)) .then(data => this.store.storeDeploymentData(data))
.catch(() => new Flash('Error getting deployment information.')), .catch(() => new Flash('Error getting deployment information.')),
]) ])
.then(() => { this.showEmptyState = false; }) .then(() => {
if (this.store.groups.length < 1) {
this.state = 'noData';
return;
}
this.showEmptyState = false;
})
.catch(() => { this.state = 'unableToConnect'; }); .catch(() => { this.state = 'unableToConnect'; });
}, },
......
...@@ -34,16 +34,23 @@ ...@@ -34,16 +34,23 @@
svgUrl: this.emptyGettingStartedSvgPath, svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring', title: 'Get started with performance monitoring',
description: `Stay updated about the performance and health description: `Stay updated about the performance and health
of your environment by configuring Prometheus to monitor your deployments.`, of your environment by configuring Prometheus to monitor your deployments.`,
buttonText: 'Configure Prometheus', buttonText: 'Configure Prometheus',
}, },
loading: { loading: {
svgUrl: this.emptyLoadingSvgPath, svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data', title: 'Waiting for performance data',
description: `Creating graphs uses the data from the Prometheus server. description: `Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`, If this takes a long time, ensure that data is available.`,
buttonText: 'View documentation', buttonText: 'View documentation',
}, },
noData: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'No data found',
description: `You are connected to the Prometheus server, but there is currently
no data to display.`,
buttonText: 'Configure Prometheus',
},
unableToConnect: { unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath, svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server', title: 'Unable to connect to Prometheus server',
......
/* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */ /* eslint-disable func-names, prefer-arrow-callback, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, max-len */
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import createFlash from './flash';
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
import syntaxHighlight from './syntax_highlight'; import syntaxHighlight from './syntax_highlight';
...@@ -60,30 +63,31 @@ export default class SingleFileDiff { ...@@ -60,30 +63,31 @@ export default class SingleFileDiff {
getContentHTML(cb) { getContentHTML(cb) {
this.collapsedContent.hide(); this.collapsedContent.hide();
this.loadingContent.show(); this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
return function(data) { axios.get(this.diffForPath)
_this.loadingContent.hide(); .then(({ data }) => {
this.loadingContent.hide();
if (data.html) { if (data.html) {
_this.content = $(data.html); this.content = $(data.html);
syntaxHighlight(_this.content); syntaxHighlight(this.content);
} else { } else {
_this.hasError = true; this.hasError = true;
_this.content = $(ERROR_HTML); this.content = $(ERROR_HTML);
} }
_this.collapsedContent.after(_this.content); this.collapsedContent.after(this.content);
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
const $file = $(_this.file); const $file = $(this.file);
FilesCommentButton.init($file); FilesCommentButton.init($file);
const canCreateNote = $file.closest('.files').is('[data-can-create-note]'); const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
imageDiffHelper.initImageDiff($file[0], canCreateNote); imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb(); if (cb) cb();
}; })
})(this)); .catch(createFlash(__('An error occurred while retrieving diff')));
} }
} }
import Flash from '../../../flash';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import tooltip from '../../../vue_shared/directives/tooltip';
import loadingIcon from '../../../vue_shared/components/loading_icon.vue';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
props: {
mr: { type: Object, required: true },
service: { type: Object, required: true },
},
data() {
return {
isMakingRequest: false,
};
},
directives: {
tooltip,
},
components: {
'mr-widget-author-and-time': mrWidgetAuthorTime,
loadingIcon,
statusIcon,
},
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.data)
.then((data) => {
if (data.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 media">
<status-icon status="success" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-and-time
actionText="Merged by"
:author="mr.metrics.mergedBy"
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt" />
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
class="btn btn-close btn-xs"
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"
v-tooltip
class="btn btn-close btn-xs"
data-method="post"
:href="mr.revertInForkPath"
title="Revert this merge request in a new merge request">
Revert
</a>
<a
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
class="btn btn-default btn-xs"
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"
v-tooltip
class="btn btn-default btn-xs"
data-method="post"
:href="mr.cherryPickInForkPath"
title="Cherry-pick this merge request in a new merge request">
Cherry-pick
</a>
</div>
<section class="mr-info-list">
<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" class="space-children">
<span>You can remove source branch now</span>
<button
@click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
class="btn btn-xs btn-default js-remove-branch-button">
Remove Source Branch
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<loading-icon inline />
<span>The source branch is being removed</span>
</p>
</section>
</div>
</div>
`,
};
<script>
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { s__, __ } from '~/locale';
import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
import statusIcon from '../mr_widget_status_icon.vue';
import eventHub from '../../event_hub';
export default {
name: 'MRWidgetMerged',
directives: {
tooltip,
},
components: {
mrWidgetAuthorTime,
loadingIcon,
statusIcon,
},
props: {
mr: {
type: Object,
required: true,
default: () => ({}),
},
service: {
type: Object,
required: true,
default: () => ({}),
},
},
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;
},
revertTitle() {
return s__('mrWidget|Revert this merge request in a new merge request');
},
cherryPickTitle() {
return s__('mrWidget|Cherry-pick this merge request in a new merge request');
},
revertLabel() {
return s__('mrWidget|Revert');
},
cherryPickLabel() {
return s__('mrWidget|Cherry-pick');
},
},
methods: {
removeSourceBranch() {
this.isMakingRequest = true;
this.service.removeSourceBranch()
.then(res => res.data)
.then((data) => {
if (data.message === 'Branch was removed') {
eventHub.$emit('MRWidgetUpdateRequested', () => {
this.isMakingRequest = false;
});
}
})
.catch(() => {
this.isMakingRequest = false;
Flash(__('Something went wrong. Please try again.'));
});
},
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-time
:action-text="s__('mrWidget|Merged by')"
:author="mr.metrics.mergedBy"
:date-title="mr.metrics.mergedAt"
:date-readable="mr.metrics.readableMergedAt"
/>
<a
v-if="mr.canRevertInCurrentMR"
v-tooltip
class="btn btn-close btn-xs"
href="#modal-revert-commit"
data-toggle="modal"
data-container="body"
:title="revertTitle"
>
{{ revertLabel }}
</a>
<a
v-else-if="mr.revertInForkPath"
v-tooltip
class="btn btn-close btn-xs"
data-method="post"
:href="mr.revertInForkPath"
:title="revertTitle"
>
{{ revertLabel }}
</a>
<a
v-if="mr.canCherryPickInCurrentMR"
v-tooltip
class="btn btn-default btn-xs"
href="#modal-cherry-pick-commit"
data-toggle="modal"
data-container="body"
:title="cherryPickTitle"
>
{{ cherryPickLabel }}
</a>
<a
v-else-if="mr.cherryPickInForkPath"
v-tooltip
class="btn btn-default btn-xs"
data-method="post"
:href="mr.cherryPickInForkPath"
:title="cherryPickTitle"
>
{{ cherryPickLabel }}
</a>
</div>
<section class="mr-info-list">
<p>
{{ s__("mrWidget|The changes were merged into") }}
<span class="label-branch">
<a :href="mr.targetBranchPath">{{ mr.targetBranch }}</a>
</span>
</p>
<p v-if="mr.sourceBranchRemoved">
{{ s__("mrWidget|The source branch has been removed") }}
</p>
<p
v-if="shouldShowRemoveSourceBranch"
class="space-children"
>
<span>{{ s__("mrWidget|You can remove source branch now") }}</span>
<button
@click="removeSourceBranch"
:disabled="isMakingRequest"
type="button"
class="btn btn-xs btn-default js-remove-branch-button"
>
{{ s__("mrWidget|Remove Source Branch") }}
</button>
</p>
<p v-if="shouldShowSourceBranchRemoving">
<loading-icon :inline="true" />
<span>
{{ s__("mrWidget|The source branch is being removed") }}
</span>
</p>
</section>
</div>
</div>
</template>
...@@ -16,7 +16,7 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help'; ...@@ -16,7 +16,7 @@ export { default as WidgetMergeHelp } from './components/mr_widget_merge_help';
export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue'; export { default as WidgetPipeline } from './components/mr_widget_pipeline.vue';
export { default as WidgetDeployment } from './components/mr_widget_deployment'; export { default as WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links'; export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
export { default as MergedState } from './components/states/mr_widget_merged'; export { default as MergedState } from './components/states/mr_widget_merged.vue';
export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue'; export { default as FailedToMerge } from './components/states/mr_widget_failed_to_merge.vue';
export { default as ClosedState } from './components/states/mr_widget_closed.vue'; export { default as ClosedState } from './components/states/mr_widget_closed.vue';
export { default as MergingState } from './components/states/mr_widget_merging.vue'; export { default as MergingState } from './components/states/mr_widget_merging.vue';
......
...@@ -466,7 +466,7 @@ module Ci ...@@ -466,7 +466,7 @@ module Ci
if cache && project.jobs_cache_index if cache && project.jobs_cache_index
cache = cache.merge( cache = cache.merge(
key: "#{cache[:key]}:#{project.jobs_cache_index}") key: "#{cache[:key]}_#{project.jobs_cache_index}")
end end
[cache] [cache]
......
...@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService ...@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService
username: self.username, username: self.username,
password: self.password, password: self.password,
site: URI.join(url, '/').to_s, site: URI.join(url, '/').to_s,
context_path: url.path, context_path: url.path.chomp('/'),
auth_type: :basic, auth_type: :basic,
read_timeout: 120, read_timeout: 120,
use_cookies: true, use_cookies: true,
......
...@@ -9,7 +9,8 @@ module MergeRequests ...@@ -9,7 +9,8 @@ module MergeRequests
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an # Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge # empty diff during a manual merge
close_merge_requests close_upon_missing_source_branch_ref
post_merge_manually_merged
reload_merge_requests reload_merge_requests
reset_merge_when_pipeline_succeeds reset_merge_when_pipeline_succeeds
mark_pending_todos_done mark_pending_todos_done
...@@ -29,11 +30,22 @@ module MergeRequests ...@@ -29,11 +30,22 @@ module MergeRequests
private private
def close_upon_missing_source_branch_ref
# MergeRequest#reload_diff ignores not opened MRs. This means it won't
# create an `empty` diff for `closed` MRs without a source branch, keeping
# the latest diff state as the last _valid_ one.
merge_requests_for_source_branch.reject(&:source_branch_exists?).each do |mr|
MergeRequests::CloseService
.new(mr.target_project, @current_user)
.execute(mr)
end
end
# Collect open merge requests that target same branch we push into # Collect open merge requests that target same branch we push into
# and close if push to master include last commit from merge request # and close if push to master include last commit from merge request
# We need this to close(as merged) merge requests that were merged into # We need this to close(as merged) merge requests that were merged into
# target branch manually # target branch manually
def close_merge_requests def post_merge_manually_merged
commit_ids = @commits.map(&:id) commit_ids = @commits.map(&:id)
merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a
merge_requests = merge_requests.select(&:diff_head_commit) merge_requests = merge_requests.select(&:diff_head_commit)
......
%li.header-new.dropdown %li.header-new.dropdown
= link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do = link_to new_project_path, class: "header-new-dropdown-toggle has-tooltip qa-new-menu-toggle", title: "New...", ref: 'tooltip', aria: { label: "New..." }, data: { toggle: 'dropdown', placement: 'bottom', container: 'body' } do
= sprite_icon('plus-square', size: 16) = sprite_icon('plus-square', size: 16)
= sprite_icon('angle-down', css_class: 'caret-down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right .dropdown-menu-nav.dropdown-menu-align-right
......
...@@ -32,5 +32,5 @@ ...@@ -32,5 +32,5 @@
= icon("pencil") = icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
= link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do = link_to project_tag_path(@project, tag.name), class: "btn btn-remove remove-row has-tooltip prepend-left-10 #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: s_('TagsPage|Delete tag'), method: :delete, data: { confirm: s_('TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?') % { tag_name: tag.name }, container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
require_relative "../lib/gitlab/upgrader"
Gitlab::Upgrader.new.execute
---
title: Fix copy/paste on iOS devices due to a bug in webkit
merge_request: 15804
author:
type: fixed
---
title: Adds spacing between edit and delete tag btn in tag list
merge_request: 16757
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fix missing "allow users to request access" option in public project permissions
merge_request: 16485
author:
type: fixed
---
title: Fix encoding issue when counting commit count
merge_request: 16637
author:
type: fixed
---
title: Fixes destination already exists, and some particular service errors on Import/Export
error
merge_request: 16714
author:
type: fixed
---
title: Close and do not reload MR diffs when source branch is deleted
merge_request:
author:
type: fixed
---
title: Return more consistent values for merge_status on MR APIs
merge_request:
author:
type: fixed
--- ---
title: Use has_table_privilege for TRIGGER on PostgreSQL title: Fix JIRA not working when a trailing slash is included
merge_request: merge_request:
author: author:
type: fixed type: fixed
...@@ -6,6 +6,7 @@ Bundler.require(:default, Rails.env) ...@@ -6,6 +6,7 @@ Bundler.require(:default, Rails.env)
module Gitlab module Gitlab
class Application < Rails::Application class Application < Rails::Application
require_dependency Rails.root.join('lib/gitlab/redis/wrapper')
require_dependency Rails.root.join('lib/gitlab/redis/cache') require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues') require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state') require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
......
if defined?(GrapeRouteHelpers) if defined?(GrapeRouteHelpers)
module GrapeRouteHelpers module GrapeRouteHelpers
module AllRoutes
# Bringing in PR https://github.com/reprah/grape-route-helpers/pull/21 due to abandonment.
#
# Without the following fix, when two helper methods are the same, but have different arguments
# (for example: api_v1_cats_owners_path(id: 1) vs api_v1_cats_owners_path(id: 1, owner_id: 2))
# if the helper method with the least number of arguments is defined first (because the route was defined first)
# then it will shadow the longer route.
#
# The fix is to sort descending by amount of arguments
def decorated_routes
@decorated_routes ||= all_routes
.map { |r| DecoratedRoute.new(r) }
.sort_by { |r| -r.dynamic_path_segments.count }
end
end
class DecoratedRoute class DecoratedRoute
# GrapeRouteHelpers gem tries to parse the versions # GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition. # from a string, not supporting Grape `version` array definition.
......
...@@ -68,7 +68,7 @@ Example response: ...@@ -68,7 +68,7 @@ Example response:
```json ```json
{ {
"file_name": "app/project.rb", "file_path": "app/project.rb",
"branch": "master" "branch": "master"
} }
``` ```
...@@ -98,7 +98,7 @@ Example response: ...@@ -98,7 +98,7 @@ Example response:
```json ```json
{ {
"file_name": "app/project.rb", "file_path": "app/project.rb",
"branch": "master" "branch": "master"
} }
``` ```
...@@ -134,15 +134,6 @@ DELETE /projects/:id/repository/files/:file_path ...@@ -134,15 +134,6 @@ DELETE /projects/:id/repository/files/:file_path
curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file' curl --request DELETE --header 'PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK' 'https://gitlab.example.com/api/v4/projects/13083/repository/files/app%2Fproject%2Erb?branch=master&author_email=author%40example.com&author_name=Firstname%20Lastname&commit_message=delete%20file'
``` ```
Example response:
```json
{
"file_name": "app/project.rb",
"branch": "master"
}
```
Parameters: Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb - `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
......
...@@ -18,7 +18,7 @@ When you create a new [project](../../index.md), GitLab sets `master` as the def ...@@ -18,7 +18,7 @@ When you create a new [project](../../index.md), GitLab sets `master` as the def
branch for your project. You can choose another branch to be your project's branch for your project. You can choose another branch to be your project's
default under your project's **Settings > General**. default under your project's **Settings > General**.
The default branch is the branched affected by the The default branch is the branch affected by the
[issue closing pattern](../../issues/automatic_issue_closing.md), [issue closing pattern](../../issues/automatic_issue_closing.md),
which means that _an issue will be closed when a merge request is merged to which means that _an issue will be closed when a merge request is merged to
the **default branch**_. the **default branch**_.
......
...@@ -507,7 +507,15 @@ module API ...@@ -507,7 +507,15 @@ module API
expose :work_in_progress?, as: :work_in_progress expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone expose :milestone, using: Entities::Milestone
expose :merge_when_pipeline_succeeds expose :merge_when_pipeline_succeeds
expose :merge_status
# Ideally we should deprecate `MergeRequest#merge_status` exposure and
# use `MergeRequest#mergeable?` instead (boolean).
# See https://gitlab.com/gitlab-org/gitlab-ce/issues/42344 for more
# information.
expose :merge_status do |merge_request|
merge_request.check_if_can_be_merged
merge_request.merge_status
end
expose :diff_head_sha, as: :sha expose :diff_head_sha, as: :sha
expose :merge_commit_sha expose :merge_commit_sha
expose :user_notes_count expose :user_notes_count
......
...@@ -5,7 +5,17 @@ module Gitlab ...@@ -5,7 +5,17 @@ module Gitlab
module Popen module Popen
extend self extend self
def popen(cmd, path = nil, vars = {}) Result = Struct.new(:cmd, :stdout, :stderr, :status, :duration)
# Returns [stdout + stderr, status]
def popen(cmd, path = nil, vars = {}, &block)
result = popen_with_detail(cmd, path, vars, &block)
[result.stdout << result.stderr, result.status&.exitstatus]
end
# Returns Result
def popen_with_detail(cmd, path = nil, vars = {})
unless cmd.is_a?(Array) unless cmd.is_a?(Array)
raise "System commands must be given as an array of strings" raise "System commands must be given as an array of strings"
end end
...@@ -18,18 +28,21 @@ module Gitlab ...@@ -18,18 +28,21 @@ module Gitlab
FileUtils.mkdir_p(path) FileUtils.mkdir_p(path)
end end
cmd_output = "" cmd_stdout = ''
cmd_status = 0 cmd_stderr = ''
cmd_status = nil
start = Time.now
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr| Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given? yield(stdin) if block_given?
stdin.close stdin.close
cmd_output << stdout.read cmd_stdout = stdout.read
cmd_output << stderr.read cmd_stderr = stderr.read
cmd_status = wait_thr.value.exitstatus cmd_status = wait_thr.value
end end
[cmd_output, cmd_status] Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
end end
end end
end end
module Gitlab
module Popen
class Runner
attr_reader :results
def initialize
@results = []
end
def run(commands, &block)
commands.each do |cmd|
# yield doesn't support blocks, so we need to use a block variable
block.call(cmd) do # rubocop:disable Performance/RedundantBlockCall
cmd_result = Gitlab::Popen.popen_with_detail(cmd)
results << cmd_result
cmd_result
end
end
end
def all_success_and_clean?
all_success? && all_stderr_empty?
end
def all_success?
results.all? { |result| result.status.success? }
end
def all_stderr_empty?
results.all? { |result| result.stderr.empty? }
end
def failed_results
results.reject { |result| result.status.success? }
end
def warned_results
results.select do |result|
result.status.success? && !result.stderr.empty?
end
end
end
end
end
# please require all dependencies below: # please require all dependencies below:
require_relative 'wrapper' unless defined?(::Gitlab::Redis::Wrapper) require_relative 'wrapper' unless defined?(::Rails) && ::Rails.root.present?
module Gitlab module Gitlab
module Redis module Redis
......
require 'rainbow/ext/string' require 'rainbow/ext/string'
require 'gitlab/utils/strong_memoize' require 'gitlab/utils/strong_memoize'
# rubocop:disable Rails/Output
module Gitlab module Gitlab
TaskFailedError = Class.new(StandardError) TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError) TaskAbortedByUserError = Class.new(StandardError)
...@@ -96,11 +97,9 @@ module Gitlab ...@@ -96,11 +97,9 @@ module Gitlab
end end
def gid_for(group_name) def gid_for(group_name)
begin Etc.getgrnam(group_name).gid
Etc.getgrnam(group_name).gid rescue ArgumentError # no group
rescue ArgumentError # no group "group #{group_name} doesn't exist"
"group #{group_name} doesn't exist"
end
end end
def gitlab_user def gitlab_user
......
require_relative "popen"
require_relative "version_info"
module Gitlab module Gitlab
class Upgrader class Upgrader
def execute def execute
......
require 'tasks/gitlab/task_helpers'
module SystemCheck module SystemCheck
module Helpers module Helpers
include ::Gitlab::TaskHelpers include ::Gitlab::TaskHelpers
......
desc 'Code duplication analyze via flay' desc 'Code duplication analyze via flay'
task :flay do task :flay do
output = `bundle exec flay --mass 35 app/ lib/gitlab/` output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}`
if output.include? "Similar code found" if output.include? "Similar code found"
puts output puts output
......
...@@ -4,7 +4,7 @@ namespace :gitlab do ...@@ -4,7 +4,7 @@ namespace :gitlab do
namespace :backup do namespace :backup do
# Create backup of GitLab system # Create backup of GitLab system
desc "GitLab | Create a backup of the GitLab system" desc "GitLab | Create a backup of the GitLab system"
task create: :environment do task create: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
configure_cron_mode configure_cron_mode
...@@ -25,7 +25,7 @@ namespace :gitlab do ...@@ -25,7 +25,7 @@ namespace :gitlab do
# Restore backup of GitLab system # Restore backup of GitLab system
desc 'GitLab | Restore a previously created backup' desc 'GitLab | Restore a previously created backup'
task restore: :environment do task restore: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
configure_cron_mode configure_cron_mode
...@@ -73,7 +73,7 @@ namespace :gitlab do ...@@ -73,7 +73,7 @@ namespace :gitlab do
end end
namespace :repo do namespace :repo do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping repositories ...".color(:blue) $progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories") if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
...@@ -84,7 +84,7 @@ namespace :gitlab do ...@@ -84,7 +84,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring repositories ...".color(:blue) $progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore Backup::Repository.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -92,7 +92,7 @@ namespace :gitlab do ...@@ -92,7 +92,7 @@ namespace :gitlab do
end end
namespace :db do namespace :db do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping database ... ".color(:blue) $progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db") if ENV["SKIP"] && ENV["SKIP"].include?("db")
...@@ -103,7 +103,7 @@ namespace :gitlab do ...@@ -103,7 +103,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring database ... ".color(:blue) $progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore Backup::Database.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -111,7 +111,7 @@ namespace :gitlab do ...@@ -111,7 +111,7 @@ namespace :gitlab do
end end
namespace :builds do namespace :builds do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping builds ... ".color(:blue) $progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds") if ENV["SKIP"] && ENV["SKIP"].include?("builds")
...@@ -122,7 +122,7 @@ namespace :gitlab do ...@@ -122,7 +122,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring builds ... ".color(:blue) $progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore Backup::Builds.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -130,7 +130,7 @@ namespace :gitlab do ...@@ -130,7 +130,7 @@ namespace :gitlab do
end end
namespace :uploads do namespace :uploads do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping uploads ... ".color(:blue) $progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads") if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
...@@ -141,7 +141,7 @@ namespace :gitlab do ...@@ -141,7 +141,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring uploads ... ".color(:blue) $progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore Backup::Uploads.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -149,7 +149,7 @@ namespace :gitlab do ...@@ -149,7 +149,7 @@ namespace :gitlab do
end end
namespace :artifacts do namespace :artifacts do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping artifacts ... ".color(:blue) $progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts") if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
...@@ -160,7 +160,7 @@ namespace :gitlab do ...@@ -160,7 +160,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring artifacts ... ".color(:blue) $progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore Backup::Artifacts.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -168,7 +168,7 @@ namespace :gitlab do ...@@ -168,7 +168,7 @@ namespace :gitlab do
end end
namespace :pages do namespace :pages do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping pages ... ".color(:blue) $progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages") if ENV["SKIP"] && ENV["SKIP"].include?("pages")
...@@ -179,7 +179,7 @@ namespace :gitlab do ...@@ -179,7 +179,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring pages ... ".color(:blue) $progress.puts "Restoring pages ... ".color(:blue)
Backup::Pages.new.restore Backup::Pages.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -187,7 +187,7 @@ namespace :gitlab do ...@@ -187,7 +187,7 @@ namespace :gitlab do
end end
namespace :lfs do namespace :lfs do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping lfs objects ... ".color(:blue) $progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs") if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
...@@ -198,7 +198,7 @@ namespace :gitlab do ...@@ -198,7 +198,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring lfs objects ... ".color(:blue) $progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore Backup::Lfs.new.restore
$progress.puts "done".color(:green) $progress.puts "done".color(:green)
...@@ -206,7 +206,7 @@ namespace :gitlab do ...@@ -206,7 +206,7 @@ namespace :gitlab do
end end
namespace :registry do namespace :registry do
task create: :environment do task create: :gitlab_environment do
$progress.puts "Dumping container registry images ... ".color(:blue) $progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
...@@ -221,7 +221,7 @@ namespace :gitlab do ...@@ -221,7 +221,7 @@ namespace :gitlab do
end end
end end
task restore: :environment do task restore: :gitlab_environment do
$progress.puts "Restoring container registry images ... ".color(:blue) $progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
......
# Temporary hack, until we migrate all checks to SystemCheck format
require 'system_check'
require 'system_check/helpers'
namespace :gitlab do namespace :gitlab do
desc 'GitLab | Check the configuration of GitLab and its environment' desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check task check: %w{gitlab:gitlab_shell:check
...@@ -12,7 +8,7 @@ namespace :gitlab do ...@@ -12,7 +8,7 @@ namespace :gitlab do
namespace :app do namespace :app do
desc 'GitLab | Check the configuration of the GitLab Rails app' desc 'GitLab | Check the configuration of the GitLab Rails app'
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [ checks = [
...@@ -43,7 +39,7 @@ namespace :gitlab do ...@@ -43,7 +39,7 @@ namespace :gitlab do
namespace :gitlab_shell do namespace :gitlab_shell do
desc "GitLab | Check the configuration of GitLab Shell" desc "GitLab | Check the configuration of GitLab Shell"
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
start_checking "GitLab Shell" start_checking "GitLab Shell"
...@@ -251,7 +247,7 @@ namespace :gitlab do ...@@ -251,7 +247,7 @@ namespace :gitlab do
namespace :sidekiq do namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq" desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
start_checking "Sidekiq" start_checking "Sidekiq"
...@@ -310,7 +306,7 @@ namespace :gitlab do ...@@ -310,7 +306,7 @@ namespace :gitlab do
namespace :incoming_email do namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email" desc "GitLab | Check the configuration of Reply by email"
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
if Gitlab.config.incoming_email.enabled if Gitlab.config.incoming_email.enabled
...@@ -333,7 +329,7 @@ namespace :gitlab do ...@@ -333,7 +329,7 @@ namespace :gitlab do
end end
namespace :ldap do namespace :ldap do
task :check, [:limit] => :environment do |_, args| task :check, [:limit] => :gitlab_environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big. # Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script. # This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100) args.with_defaults(limit: 100)
...@@ -389,7 +385,7 @@ namespace :gitlab do ...@@ -389,7 +385,7 @@ namespace :gitlab do
namespace :repo do namespace :repo do
desc "GitLab | Check the integrity of the repositories managed by GitLab" desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do task check: :gitlab_environment do
puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red) puts "This task is deprecated. Please use gitlab:git:fsck instead".color(:red)
Rake::Task["gitlab:git:fsck"].execute Rake::Task["gitlab:git:fsck"].execute
end end
...@@ -397,7 +393,7 @@ namespace :gitlab do ...@@ -397,7 +393,7 @@ namespace :gitlab do
namespace :orphans do namespace :orphans do
desc 'Gitlab | Check for orphaned namespaces and repositories' desc 'Gitlab | Check for orphaned namespaces and repositories'
task check: :environment do task check: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [ checks = [
SystemCheck::Orphans::NamespaceCheck, SystemCheck::Orphans::NamespaceCheck,
...@@ -408,7 +404,7 @@ namespace :gitlab do ...@@ -408,7 +404,7 @@ namespace :gitlab do
end end
desc 'GitLab | Check for orphaned namespaces in the repositories path' desc 'GitLab | Check for orphaned namespaces in the repositories path'
task check_namespaces: :environment do task check_namespaces: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [SystemCheck::Orphans::NamespaceCheck] checks = [SystemCheck::Orphans::NamespaceCheck]
...@@ -416,7 +412,7 @@ namespace :gitlab do ...@@ -416,7 +412,7 @@ namespace :gitlab do
end end
desc 'GitLab | Check for orphaned repositories in the repositories path' desc 'GitLab | Check for orphaned repositories in the repositories path'
task check_repositories: :environment do task check_repositories: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
checks = [SystemCheck::Orphans::RepositoryCheck] checks = [SystemCheck::Orphans::RepositoryCheck]
...@@ -426,7 +422,7 @@ namespace :gitlab do ...@@ -426,7 +422,7 @@ namespace :gitlab do
namespace :user do namespace :user do
desc "GitLab | Check the integrity of a specific user's repositories" desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args| task :check_repos, [:username] => :gitlab_environment do |t, args|
username = args[:username] || prompt("Check repository integrity for username? ".color(:blue)) username = args[:username] || prompt("Check repository integrity for username? ".color(:blue))
user = User.find_by(username: username) user = User.find_by(username: username)
if user if user
......
...@@ -5,7 +5,7 @@ namespace :gitlab do ...@@ -5,7 +5,7 @@ namespace :gitlab do
HASHED_REPOSITORY_NAME = '@hashed'.freeze HASHED_REPOSITORY_NAME = '@hashed'.freeze
desc "GitLab | Cleanup | Clean namespaces" desc "GitLab | Cleanup | Clean namespaces"
task dirs: :environment do task dirs: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
remove_flag = ENV['REMOVE'] remove_flag = ENV['REMOVE']
...@@ -49,7 +49,7 @@ namespace :gitlab do ...@@ -49,7 +49,7 @@ namespace :gitlab do
end end
desc "GitLab | Cleanup | Clean repositories" desc "GitLab | Cleanup | Clean repositories"
task repos: :environment do task repos: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}" move_suffix = "+orphaned+#{Time.now.to_i}"
...@@ -78,7 +78,7 @@ namespace :gitlab do ...@@ -78,7 +78,7 @@ namespace :gitlab do
end end
desc "GitLab | Cleanup | Block users that have been removed in LDAP" desc "GitLab | Cleanup | Block users that have been removed in LDAP"
task block_removed_ldap_users: :environment do task block_removed_ldap_users: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
block_flag = ENV['BLOCK'] block_flag = ENV['BLOCK']
...@@ -109,7 +109,7 @@ namespace :gitlab do ...@@ -109,7 +109,7 @@ namespace :gitlab do
# released. So likely this should only be run once on gitlab.com # released. So likely this should only be run once on gitlab.com
# Faulty refs are moved so they are kept around, else some features break. # Faulty refs are moved so they are kept around, else some features break.
desc 'GitLab | Cleanup | Remove faulty deployment refs' desc 'GitLab | Cleanup | Remove faulty deployment refs'
task move_faulty_deployment_refs: :environment do task move_faulty_deployment_refs: :gitlab_environment do
projects = Project.where(id: Deployment.select(:project_id).distinct) projects = Project.where(id: Deployment.select(:project_id).distinct)
projects.find_each do |project| projects.find_each do |project|
......
namespace :gitlab do namespace :gitlab do
namespace :git do namespace :git do
desc "GitLab | Git | Repack" desc "GitLab | Git | Repack"
task repack: :environment do task repack: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo") failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} repack -a --quiet), "Repacking repo")
if failures.empty? if failures.empty?
puts "Done".color(:green) puts "Done".color(:green)
...@@ -11,7 +11,7 @@ namespace :gitlab do ...@@ -11,7 +11,7 @@ namespace :gitlab do
end end
desc "GitLab | Git | Run garbage collection on all repos" desc "GitLab | Git | Run garbage collection on all repos"
task gc: :environment do task gc: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting") failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} gc --auto --quiet), "Garbage Collecting")
if failures.empty? if failures.empty?
puts "Done".color(:green) puts "Done".color(:green)
...@@ -21,7 +21,7 @@ namespace :gitlab do ...@@ -21,7 +21,7 @@ namespace :gitlab do
end end
desc "GitLab | Git | Prune all repos" desc "GitLab | Git | Prune all repos"
task prune: :environment do task prune: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune") failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} prune), "Git Prune")
if failures.empty? if failures.empty?
puts "Done".color(:green) puts "Done".color(:green)
...@@ -31,7 +31,7 @@ namespace :gitlab do ...@@ -31,7 +31,7 @@ namespace :gitlab do
end end
desc 'GitLab | Git | Check all repos integrity' desc 'GitLab | Git | Check all repos integrity'
task fsck: :environment do task fsck: :gitlab_environment do
failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo| failures = perform_git_cmd(%W(#{Gitlab.config.git.bin_path} fsck --name-objects --no-progress), "Checking integrity") do |repo|
check_config_lock(repo) check_config_lock(repo)
check_ref_locks(repo) check_ref_locks(repo)
......
namespace :gitlab do namespace :gitlab do
namespace :gitaly do namespace :gitaly do
desc "GitLab | Install or upgrade gitaly" desc "GitLab | Install or upgrade gitaly"
task :install, [:dir, :repo] => :environment do |t, args| task :install, [:dir, :repo] => :gitlab_environment do |t, args|
require 'toml' require 'toml'
warn_user_is_not_gitlab warn_user_is_not_gitlab
......
require 'tasks/gitlab/task_helpers'
# Prevent StateMachine warnings from outputting during a cron task # Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON'] StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
namespace :gitlab do task gitlab_environment: :environment do
extend SystemCheck::Helpers extend SystemCheck::Helpers
end end
namespace :gitlab do namespace :gitlab do
namespace :env do namespace :env do
desc "GitLab | Show information about GitLab and its environment" desc "GitLab | Show information about GitLab and its environment"
task info: :environment do task info: :gitlab_environment do
# check if there is an RVM environment # check if there is an RVM environment
rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s) rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version # check Ruby version
......
namespace :gitlab do namespace :gitlab do
desc "GitLab | Setup production application" desc "GitLab | Setup production application"
task setup: :environment do task setup: :gitlab_environment do
setup_db setup_db
end end
......
namespace :gitlab do namespace :gitlab do
namespace :shell do namespace :shell do
desc "GitLab | Install or upgrade gitlab-shell" desc "GitLab | Install or upgrade gitlab-shell"
task :install, [:repo] => :environment do |t, args| task :install, [:repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab warn_user_is_not_gitlab
default_version = Gitlab::Shell.version_required default_version = Gitlab::Shell.version_required
...@@ -58,12 +58,12 @@ namespace :gitlab do ...@@ -58,12 +58,12 @@ namespace :gitlab do
end end
desc "GitLab | Setup gitlab-shell" desc "GitLab | Setup gitlab-shell"
task setup: :environment do task setup: :gitlab_environment do
setup setup
end end
desc "GitLab | Build missing projects" desc "GitLab | Build missing projects"
task build_missing_projects: :environment do task build_missing_projects: :gitlab_environment do
Project.find_each(batch_size: 1000) do |project| Project.find_each(batch_size: 1000) do |project|
path_to_repo = project.repository.path_to_repo path_to_repo = project.repository.path_to_repo
if File.exist?(path_to_repo) if File.exist?(path_to_repo)
...@@ -80,7 +80,7 @@ namespace :gitlab do ...@@ -80,7 +80,7 @@ namespace :gitlab do
end end
desc 'Create or repair repository hooks symlink' desc 'Create or repair repository hooks symlink'
task create_hooks: :environment do task create_hooks: :gitlab_environment do
warn_user_is_not_gitlab warn_user_is_not_gitlab
puts 'Creating/Repairing hooks symlinks for all repositories' puts 'Creating/Repairing hooks symlinks for all repositories'
......
namespace :gitlab do namespace :gitlab do
namespace :workhorse do namespace :workhorse do
desc "GitLab | Install or upgrade gitlab-workhorse" desc "GitLab | Install or upgrade gitlab-workhorse"
task :install, [:dir, :repo] => :environment do |t, args| task :install, [:dir, :repo] => :gitlab_environment do |t, args|
warn_user_is_not_gitlab warn_user_is_not_gitlab
unless args.dir.present? unless args.dir.present?
......
...@@ -2,5 +2,14 @@ unless Rails.env.production? ...@@ -2,5 +2,14 @@ unless Rails.env.production?
require 'haml_lint/rake_task' require 'haml_lint/rake_task'
require 'haml_lint/inline_javascript' require 'haml_lint/inline_javascript'
# Workaround for warnings from parser/current
# TODO: Remove this after we update parser gem
task :haml_lint do
require 'parser'
def Parser.warn(*args)
puts(*args) # static-analysis ignores stdout if status is 0
end
end
HamlLint::RakeTask.new HamlLint::RakeTask.new
end end
require Rails.root.join('lib/gitlab/database')
require Rails.root.join('lib/gitlab/database/migration_helpers')
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
desc 'GitLab | Sets up PostgreSQL' desc 'GitLab | Sets up PostgreSQL'
task setup_postgresql: :environment do task setup_postgresql: :environment do
require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lower_indexes')
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
require Rails.root.join('db/migrate/20171220191323_add_index_on_namespaces_lower_name.rb')
require Rails.root.join('db/migrate/20180113220114_rework_redirect_routes_indexes.rb')
NamespacesProjectsPathLowerIndexes.new.up NamespacesProjectsPathLowerIndexes.new.up
AddUsersLowerUsernameEmailIndexes.new.up AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up AddLowerPathIndexToRoutes.new.up
......
...@@ -27,6 +27,7 @@ module QA ...@@ -27,6 +27,7 @@ module QA
module Resource module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox' autoload :Sandbox, 'qa/factory/resource/sandbox'
autoload :Group, 'qa/factory/resource/group' autoload :Group, 'qa/factory/resource/group'
autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project' autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request' autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key' autoload :DeployKey, 'qa/factory/resource/deploy_key'
...@@ -125,6 +126,12 @@ module QA ...@@ -125,6 +126,12 @@ module QA
autoload :SecretVariables, 'qa/page/project/settings/secret_variables' autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners' autoload :Runners, 'qa/page/project/settings/runners'
end end
module Issue
autoload :New, 'qa/page/project/issue/new'
autoload :Show, 'qa/page/project/issue/show'
autoload :Index, 'qa/page/project/issue/index'
end
end end
module Profile module Profile
......
require 'securerandom'
module QA
module Factory
module Resource
class Issue < Factory::Base
attr_writer :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
product :title do
Page::Project::Issue::Show.act { issue_title }
end
def fabricate!
project.visit!
Page::Project::Show.act do
go_to_new_issue
end
Page::Project::Issue::New.perform do |page|
page.add_title(@title)
page.add_description(@description)
page.create_new_issue
end
end
end
end
end
end
...@@ -42,6 +42,23 @@ module QA ...@@ -42,6 +42,23 @@ module QA
page.within(selector) { yield } if block_given? page.within(selector) { yield } if block_given?
end end
# Returns true if successfully GETs the given URL
# Useful because `page.status_code` is unsupported by our driver, and
# we don't have access to the `response` to use `have_http_status`.
def asset_exists?(url)
page.execute_script <<~JS
xhr = new XMLHttpRequest();
xhr.open('GET', '#{url}', true);
xhr.send();
JS
return false unless wait(time: 0.5, max: 60, reload: false) do
page.evaluate_script('xhr.readyState == XMLHttpRequest.DONE')
end
page.evaluate_script('xhr.status') == 200
end
def find_element(name) def find_element(name)
find(element_selector_css(name)) find(element_selector_css(name))
end end
...@@ -80,6 +97,21 @@ module QA ...@@ -80,6 +97,21 @@ module QA
views.map(&:errors).flatten views.map(&:errors).flatten
end end
# Not tested and not expected to work with multiple dropzones
# instantiated on one page because there is no distinguishing
# attribute per dropzone file field.
def attach_file_to_dropzone(attachment, dropzone_form_container)
filename = File.basename(attachment)
field_style = { visibility: 'visible', height: '', width: '' }
attach_file(attachment, class: 'dz-hidden-input', make_visible: field_style)
# Wait for link to be appended to dropzone text
wait(reload: false) do
find("#{dropzone_form_container} textarea").value.match(filename)
end
end
class DSL class DSL
attr_reader :views attr_reader :views
......
...@@ -3,23 +3,14 @@ module QA ...@@ -3,23 +3,14 @@ module QA
module Group module Group
class Show < Page::Base class Show < Page::Base
view 'app/views/groups/show.html.haml' do view 'app/views/groups/show.html.haml' do
element :dropdown_toggle, '.dropdown-toggle' element :new_project_or_subgroup_dropdown, '.new-project-subgroup'
element :new_project_subgroup, '.new-project-subgroup' element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle'
element :new_project_option, /%li.*data:.*value: "new-project"/
element :new_project_toggle, element :new_project_button, /%input.*data:.*action: "new-project"/
/%li.+ data: { value: "new\-project"/ element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/
element :new_project_button,
/%input.+ data: { action: "new\-project"/ # data-value and data-action get modified by JS for subgroup
element :new_subgroup_button, /%input.*\.js-new-group-child/
element :new_subgroup_toggle,
/%li.+ data: { value: "new\-subgroup"/
# TODO: input[data-action='new-subgroup'] seems to be handled by JS?
# See app/assets/javascripts/groups/new_group_child.js
end
view 'app/views/shared/groups/_search_form.html.haml' do
element :filter_by_name,
"placeholder: s_('GroupsTree|Filter by name...')"
end end
def go_to_subgroup(name) def go_to_subgroup(name)
......
...@@ -7,6 +7,8 @@ module QA ...@@ -7,6 +7,8 @@ module QA
element :settings_link, 'link_to edit_project_path' element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'" element :repository_link, "title: 'Repository'"
element :pipelines_settings_link, "title: 'CI / CD'" element :pipelines_settings_link, "title: 'CI / CD'"
element :issues_link, %r{link_to.*shortcuts-issues}
element :issues_link_text, "Issues"
element :top_level_items, '.sidebar-top-level-items' element :top_level_items, '.sidebar-top-level-items'
element :activity_link, "title: 'Activity'" element :activity_link, "title: 'Activity'"
end end
...@@ -43,6 +45,12 @@ module QA ...@@ -43,6 +45,12 @@ module QA
end end
end end
def click_issues
within_sidebar do
click_link('Issues')
end
end
private private
def hover_settings def hover_settings
......
module QA
module Page
module Project
module Issue
class Index < Page::Base
view 'app/views/projects/issues/_issue.html.haml' do
element :issue_link, 'link_to issue.title'
end
def go_to_issue(title)
click_link(title)
end
end
end
end
end
end
module QA
module Page
module Project
module Issue
class New < Page::Base
view 'app/views/shared/issuable/_form.html.haml' do
element :submit_issue_button, 'form.submit "Submit'
end
view 'app/views/shared/issuable/form/_title.html.haml' do
element :issue_title_textbox, 'form.text_field :title'
end
view 'app/views/shared/form_elements/_description.html.haml' do
element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description"
end
def add_title(title)
fill_in 'issue_title', with: title
end
def add_description(description)
fill_in 'issue_description', with: description
end
def create_new_issue
click_on 'Submit issue'
end
end
end
end
end
end
module QA
module Page
module Project
module Issue
class Show < Page::Base
view 'app/views/projects/issues/show.html.haml' do
element :issue_details, '.issue-details'
element :title, '.title'
end
view 'app/views/shared/notes/_form.html.haml' do
element :new_note_form, 'new-note'
element :new_note_form, 'attr: :note'
end
view 'app/views/shared/notes/_comment_button.html.haml' do
element :comment_button, '%strong Comment'
end
def issue_title
find('.issue-details .title').text
end
# Adds a comment to an issue
# attachment option should be an absolute path
def comment(text, attachment:)
fill_in(with: text, name: 'note[note]')
attach_file_to_dropzone(attachment, '.new-note') if attachment
click_on 'Comment'
end
end
end
end
end
end
...@@ -17,6 +17,11 @@ module QA ...@@ -17,6 +17,11 @@ module QA
element :project_name element :project_name
end end
view 'app/views/layouts/header/_new_dropdown.haml' do
element :new_menu_toggle
element :new_issue_link, "link_to 'New issue', new_project_issue_path(@project)"
end
def choose_repository_clone_http def choose_repository_clone_http
wait(reload: false) do wait(reload: false) do
click_element :clone_dropdown click_element :clone_dropdown
...@@ -46,6 +51,12 @@ module QA ...@@ -46,6 +51,12 @@ module QA
sleep 5 sleep 5
refresh refresh
end end
def go_to_new_issue
click_element :new_menu_toggle
click_link 'New issue'
end
end end
end end
end end
......
#!/usr/bin/env ruby #!/usr/bin/env ruby
require ::File.expand_path('../lib/gitlab/popen', __dir__) # We don't have auto-loading here
require_relative '../lib/gitlab/popen'
require_relative '../lib/gitlab/popen/runner'
def emit_warnings(static_analysis)
static_analysis.warned_results.each do |result|
puts
puts "**** #{result.cmd.join(' ')} had the following warnings:"
puts
puts result.stderr
puts
end
end
def emit_errors(static_analysis)
static_analysis.failed_results.each do |result|
puts
puts "**** #{result.cmd.join(' ')} failed with the following error:"
puts
puts result.stdout
puts result.stderr
puts
end
end
tasks = [ tasks = [
%w[bundle exec rake config_lint], %w[bundle exec rake config_lint],
...@@ -12,23 +35,20 @@ tasks = [ ...@@ -12,23 +35,20 @@ tasks = [
%w[bundle exec rubocop --parallel], %w[bundle exec rubocop --parallel],
%w[bundle exec rake gettext:lint], %w[bundle exec rake gettext:lint],
%w[bundle exec rake lint:static_verification], %w[bundle exec rake lint:static_verification],
%w[scripts/lint-changelog-yaml],
%w[scripts/lint-conflicts.sh], %w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged] %w[scripts/lint-rugged]
] ]
failed_tasks = tasks.reduce({}) do |failures, task| static_analysis = Gitlab::Popen::Runner.new
start = Time.now
puts
puts "$ #{task.join(' ')}"
output, status = Gitlab::Popen.popen(task) static_analysis.run(tasks) do |cmd, &run|
puts "==> Finished in #{Time.now - start} seconds"
puts puts
puts "$ #{cmd.join(' ')}"
failures[task.join(' ')] = output unless status.zero? result = run.call
failures puts "==> Finished in #{result.duration} seconds"
puts
end end
puts puts
...@@ -36,17 +56,20 @@ puts '===================================================' ...@@ -36,17 +56,20 @@ puts '==================================================='
puts puts
puts puts
if failed_tasks.empty? if static_analysis.all_success_and_clean?
puts 'All static analyses passed successfully.' puts 'All static analyses passed successfully.'
elsif static_analysis.all_success?
puts 'All static analyses passed successfully, but we have warnings:'
puts
emit_warnings(static_analysis)
exit 2
else else
puts 'Some static analyses failed:' puts 'Some static analyses failed:'
failed_tasks.each do |failed_task, output| emit_warnings(static_analysis)
puts emit_errors(static_analysis)
puts "**** #{failed_task} failed with the following error:"
puts
puts output
end
exit 1 exit 1
end end
...@@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do ...@@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do
wait_for_requests wait_for_requests
end end
it 'makes a request to get the content' do
ajax_uris = evaluate_script('ajaxUris')
expect(ajax_uris).not_to be_empty
expect(ajax_uris.first).to include('large_diff.md')
end
it 'shows the diff content' do it 'shows the diff content' do
expect(large_diff).to have_selector('.code') expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block') expect(large_diff).not_to have_selector('.nothing-here-block')
......
...@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do ...@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do
sign_in(user) sign_in(user)
end end
def edit_and_commit def edit_and_commit(commit_changes: true)
wait_for_requests wait_for_requests
find('.js-edit-blob').click find('.js-edit-blob').click
find('#editor') find('#editor')
execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")') execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
click_button 'Commit changes'
if commit_changes
click_button 'Commit changes'
end
end end
context 'from MR diff' do context 'from MR diff' do
...@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do ...@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do
context 'from blob file path' do context 'from blob file path' do
before do before do
visit project_blob_path(project, tree_join(branch, file_path)) visit project_blob_path(project, tree_join(branch, file_path))
edit_and_commit
end end
it 'updates content' do it 'updates content' do
edit_and_commit
expect(page).to have_content 'successfully committed' expect(page).to have_content 'successfully committed'
expect(page).to have_content 'NextFeature' expect(page).to have_content 'NextFeature'
end end
it 'previews content' do
edit_and_commit(commit_changes: false)
click_link 'Preview changes'
wait_for_requests
old_line_count = page.all('.line_holder.old').size
new_line_count = page.all('.line_holder.new').size
expect(old_line_count).to be > 0
expect(new_line_count).to be > 0
end
end end
end end
......
require 'spec_helper'
require_relative '../../config/initializers/grape_route_helpers_fix'
describe 'route shadowing' do
include GrapeRouteHelpers::NamedRouteMatcher
it 'does not occur' do
path = api_v4_projects_merge_requests_path(id: 1)
expect(path).to eq('/api/v4/projects/1/merge_requests')
path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
expect(path).to eq('/api/v4/projects/1/merge_requests/3')
end
end
...@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => { ...@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => {
preloadFixtures('static/create_item_dropdown.html.raw'); preloadFixtures('static/create_item_dropdown.html.raw');
let $wrapperEl; let $wrapperEl;
let createItemDropdown;
function createItemAndClearInput(text) {
// Filter for the new item
$wrapperEl.find('.dropdown-input-field')
.val(text)
.trigger('input');
// Create the new item
const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
$createButton.click();
// Clear out the filter
$wrapperEl.find('.dropdown-input-field')
.val('')
.trigger('input');
}
beforeEach(() => { beforeEach(() => {
loadFixtures('static/create_item_dropdown.html.raw'); loadFixtures('static/create_item_dropdown.html.raw');
$wrapperEl = $('.js-create-item-dropdown-fixture-root'); $wrapperEl = $('.js-create-item-dropdown-fixture-root');
// eslint-disable-next-line no-new
new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
}); });
afterEach(() => { afterEach(() => {
$wrapperEl.remove(); $wrapperEl.remove();
}); });
it('should have a dropdown item for each piece of data', () => { describe('items', () => {
// Get the data in the dropdown beforeEach(() => {
$('.js-dropdown-menu-toggle').click(); createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
});
it('should have a dropdown item for each piece of data', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
const $itemEls = $wrapperEl.find('.js-dropdown-content a'); const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
});
}); });
describe('created items', () => { describe('created items', () => {
const NEW_ITEM_TEXT = 'foobarbaz'; const NEW_ITEM_TEXT = 'foobarbaz';
function createItemAndClearInput(text) {
// Filter for the new item
$wrapperEl.find('.dropdown-input-field')
.val(text)
.trigger('input');
// Create the new item
const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
$createButton.click();
// Clear out the filter
$wrapperEl.find('.dropdown-input-field')
.val('')
.trigger('input');
}
beforeEach(() => { beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
// Open the dropdown // Open the dropdown
$('.js-dropdown-menu-toggle').click(); $('.js-dropdown-menu-toggle').click();
...@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => { ...@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => {
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length); expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
}); });
}); });
describe('clearDropdown()', () => {
beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
});
});
it('should clear all data and filter input', () => {
const filterInput = $wrapperEl.find('.dropdown-input-field');
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
// Filter for an item
filterInput
.val('one')
.trigger('input');
const $itemElsAfterFilter = $wrapperEl.find('.js-dropdown-content a');
expect($itemElsAfterFilter.length).toEqual(1);
createItemDropdown.clearDropdown();
const $itemElsAfterClear = $wrapperEl.find('.js-dropdown-content a');
expect($itemElsAfterClear.length).toEqual(0);
expect(filterInput.val()).toEqual('');
});
});
describe('createNewItemFromValue option', () => {
beforeEach(() => {
createItemDropdown = new CreateItemDropdown({
$dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
defaultToggleLabel: 'All variables',
fieldName: 'variable[environment]',
getData: (term, callback) => {
callback(DROPDOWN_ITEM_DATA);
},
createNewItemFromValue: newValue => ({
title: `${newValue}-title`,
id: `${newValue}-id`,
text: `${newValue}-text`,
}),
});
});
it('all items go through createNewItemFromValue', () => {
// Get the data in the dropdown
$('.js-dropdown-menu-toggle').click();
createItemAndClearInput('new-item');
const $itemEls = $wrapperEl.find('.js-dropdown-content a');
expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length);
expect($($itemEls[3]).text()).toEqual('new-item-text');
expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title');
});
});
}); });
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form'; import IntegrationSettingsForm from '~/integrations/integration_settings_form';
describe('IntegrationSettingsForm', () => { describe('IntegrationSettingsForm', () => {
...@@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => { ...@@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => {
describe('testSettings', () => { describe('testSettings', () => {
let integrationSettingsForm; let integrationSettingsForm;
let formData; let formData;
let mock;
beforeEach(() => { beforeEach(() => {
mock = new MockAdaptor(axios);
spyOn(axios, 'put').and.callThrough();
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
formData = integrationSettingsForm.$form.serialize(); formData = integrationSettingsForm.$form.serialize();
}); });
it('should make an ajax request with provided `formData`', () => { afterEach(() => {
const deferred = $.Deferred(); mock.restore();
spyOn($, 'ajax').and.returnValue(deferred.promise()); });
integrationSettingsForm.testSettings(formData); it('should make an ajax request with provided `formData`', (done) => {
integrationSettingsForm.testSettings(formData)
.then(() => {
expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
expect($.ajax).toHaveBeenCalledWith({ done();
type: 'PUT', })
url: integrationSettingsForm.testEndPoint, .catch(done.fail);
data: formData,
});
}); });
it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => { it('should show error Flash with `Save anyway` action if ajax request responds with error in test', (done) => {
const errorMessage = 'Test failed.'; const errorMessage = 'Test failed.';
const deferred = $.Deferred(); mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
spyOn($, 'ajax').and.returnValue(deferred.promise()); error: true,
message: errorMessage,
integrationSettingsForm.testSettings(formData); service_response: 'some error',
});
deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' }); integrationSettingsForm.testSettings(formData)
.then(() => {
const $flashContainer = $('.flash-container');
expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
expect($flashContainer.find('.flash-action')).toBeDefined();
expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
const $flashContainer = $('.flash-container'); done();
expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error'); })
expect($flashContainer.find('.flash-action')).toBeDefined(); .catch(done.fail);
expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
}); });
it('should submit form if ajax request responds without any error in test', () => { it('should submit form if ajax request responds without any error in test', (done) => {
const deferred = $.Deferred(); spyOn(integrationSettingsForm.$form, 'submit');
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData); mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: false,
});
spyOn(integrationSettingsForm.$form, 'submit'); integrationSettingsForm.testSettings(formData)
deferred.resolve({ error: false }); .then(() => {
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); done();
})
.catch(done.fail);
}); });
it('should submit form when clicked on `Save anyway` action of error Flash', () => { it('should submit form when clicked on `Save anyway` action of error Flash', (done) => {
const errorMessage = 'Test failed.'; spyOn(integrationSettingsForm.$form, 'submit');
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData); const errorMessage = 'Test failed.';
mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
error: true,
message: errorMessage,
});
deferred.resolve({ error: true, message: errorMessage }); integrationSettingsForm.testSettings(formData)
.then(() => {
const $flashAction = $('.flash-container .flash-action');
expect($flashAction).toBeDefined();
const $flashAction = $('.flash-container .flash-action'); $flashAction.get(0).click();
expect($flashAction).toBeDefined(); })
.then(() => {
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
spyOn(integrationSettingsForm.$form, 'submit'); done();
$flashAction.get(0).click(); })
expect(integrationSettingsForm.$form.submit).toHaveBeenCalled(); .catch(done.fail);
}); });
it('should show error Flash if ajax request failed', () => { it('should show error Flash if ajax request failed', (done) => {
const errorMessage = 'Something went wrong on our end.'; const errorMessage = 'Something went wrong on our end.';
const deferred = $.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData); mock.onPut(integrationSettingsForm.testEndPoint).networkError();
deferred.reject(); integrationSettingsForm.testSettings(formData)
.then(() => {
expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage); done();
})
.catch(done.fail);
}); });
it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => { it('should always call `toggleSubmitBtnState` with `false` once request is completed', (done) => {
const deferred = $.Deferred(); mock.onPut(integrationSettingsForm.testEndPoint).networkError();
spyOn($, 'ajax').and.returnValue(deferred.promise());
integrationSettingsForm.testSettings(formData);
spyOn(integrationSettingsForm, 'toggleSubmitBtnState'); spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
deferred.reject();
expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false); integrationSettingsForm.testSettings(formData)
.then(() => {
expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
done();
})
.catch(done.fail);
}); });
}); });
}); });
import MockAdaptor from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index'; import IssuableIndex from '~/issuable_index';
describe('Issuable', () => { describe('Issuable', () => {
...@@ -19,6 +21,8 @@ describe('Issuable', () => { ...@@ -19,6 +21,8 @@ describe('Issuable', () => {
}); });
describe('resetIncomingEmailToken', () => { describe('resetIncomingEmailToken', () => {
let mock;
beforeEach(() => { beforeEach(() => {
const element = document.createElement('a'); const element = document.createElement('a');
element.classList.add('incoming-email-token-reset'); element.classList.add('incoming-email-token-reset');
...@@ -30,14 +34,28 @@ describe('Issuable', () => { ...@@ -30,14 +34,28 @@ describe('Issuable', () => {
document.body.appendChild(input); document.body.appendChild(input);
Issuable = new IssuableIndex('issue_'); Issuable = new IssuableIndex('issue_');
mock = new MockAdaptor(axios);
mock.onPut('foo').reply(200, {
new_address: 'testing123',
});
}); });
it('should send request to reset email token', () => { afterEach(() => {
spyOn(jQuery, 'ajax').and.callThrough(); mock.restore();
});
it('should send request to reset email token', (done) => {
spyOn(axios, 'put').and.callThrough();
document.querySelector('.incoming-email-token-reset').click(); document.querySelector('.incoming-email-token-reset').click();
expect(jQuery.ajax).toHaveBeenCalled(); setTimeout(() => {
expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo'); expect(axios.put).toHaveBeenCalledWith('foo');
expect($('#issuable_email').val()).toBe('testing123');
done();
});
}); });
}); });
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged'; import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub'; import eventHub from '~/vue_merge_request_widget/event_hub';
import mountComponent from '../../../helpers/vue_mount_component_helper';
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,
metrics: {
mergedBy: {},
mergedAt: 'mergedUpdatedAt',
readableMergedAt: '',
closedBy: {},
closedAt: 'mergedUpdatedAt',
readableClosedAt: '',
},
updatedAt: 'mrUpdatedAt',
targetBranch,
};
const service = {
removeSourceBranch() {},
};
return new Component({
el: document.createElement('div'),
propsData: { mr, service },
});
};
describe('MRWidgetMerged', () => { describe('MRWidgetMerged', () => {
describe('props', () => { let vm;
it('should have props', () => { const targetBranch = 'foo';
const { mr, service } = mergedComponent.props;
beforeEach(() => {
expect(mr.type instanceof Object).toBeTruthy(); const Component = Vue.extend(mergedComponent);
expect(mr.required).toBeTruthy(); const mr = {
isRemovingSourceBranch: false,
expect(service.type instanceof Object).toBeTruthy(); cherryPickInForkPath: false,
expect(service.required).toBeTruthy(); canCherryPickInCurrentMR: true,
}); revertInForkPath: false,
}); canRevertInCurrentMR: true,
canRemoveSourceBranch: true,
describe('components', () => { sourceBranchRemoved: true,
it('should have components added', () => { metrics: {
expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined(); mergedBy: {
}); name: 'Administrator',
username: 'root',
webUrl: 'http://localhost:3000/root',
avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
readableMergedAt: '',
closedBy: {},
closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
readableClosedAt: '',
},
updatedAt: 'mergedUpdatedAt',
targetBranch,
};
const service = {
removeSourceBranch() {},
};
spyOn(eventHub, '$emit');
vm = mountComponent(Component, { mr, service });
}); });
describe('data', () => { afterEach(() => {
it('should have default data', () => { vm.$destroy();
const data = mergedComponent.data();
expect(data.isMakingRequest).toBeFalsy();
});
}); });
describe('computed', () => { describe('computed', () => {
describe('shouldShowRemoveSourceBranch', () => { describe('shouldShowRemoveSourceBranch', () => {
it('should correct value when fields changed', () => { it('returns true when sourceBranchRemoved is false', () => {
const vm = createComponent();
vm.mr.sourceBranchRemoved = false; vm.mr.sourceBranchRemoved = false;
expect(vm.shouldShowRemoveSourceBranch).toBeTruthy(); expect(vm.shouldShowRemoveSourceBranch).toEqual(true);
});
it('returns false wehn sourceBranchRemoved is true', () => {
vm.mr.sourceBranchRemoved = true; vm.mr.sourceBranchRemoved = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
});
it('returns false when canRemoveSourceBranch is false', () => {
vm.mr.sourceBranchRemoved = false; vm.mr.sourceBranchRemoved = false;
vm.mr.canRemoveSourceBranch = false; vm.mr.canRemoveSourceBranch = false;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
});
it('returns false when is making request', () => {
vm.mr.canRemoveSourceBranch = true; vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true; vm.isMakingRequest = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
});
it('returns true when all are true', () => {
vm.mr.isRemovingSourceBranch = true; vm.mr.isRemovingSourceBranch = true;
vm.mr.canRemoveSourceBranch = true; vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true; vm.isMakingRequest = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
}); });
}); });
describe('shouldShowSourceBranchRemoving', () => { describe('shouldShowSourceBranchRemoving', () => {
it('should correct value when fields changed', () => { it('should correct value when fields changed', () => {
const vm = createComponent();
vm.mr.sourceBranchRemoved = false; vm.mr.sourceBranchRemoved = false;
expect(vm.shouldShowSourceBranchRemoving).toBeFalsy(); expect(vm.shouldShowSourceBranchRemoving).toEqual(false);
vm.mr.sourceBranchRemoved = true; vm.mr.sourceBranchRemoved = true;
expect(vm.shouldShowRemoveSourceBranch).toBeFalsy(); expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
vm.mr.sourceBranchRemoved = false; vm.mr.sourceBranchRemoved = false;
vm.isMakingRequest = true; vm.isMakingRequest = true;
expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
vm.isMakingRequest = false; vm.isMakingRequest = false;
vm.mr.isRemovingSourceBranch = true; vm.mr.isRemovingSourceBranch = true;
expect(vm.shouldShowSourceBranchRemoving).toBeTruthy(); expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
}); });
}); });
}); });
...@@ -110,8 +101,6 @@ describe('MRWidgetMerged', () => { ...@@ -110,8 +101,6 @@ describe('MRWidgetMerged', () => {
describe('methods', () => { describe('methods', () => {
describe('removeSourceBranch', () => { describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => { 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) => { spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => {
resolve({ resolve({
data: { data: {
...@@ -123,7 +112,7 @@ describe('MRWidgetMerged', () => { ...@@ -123,7 +112,7 @@ describe('MRWidgetMerged', () => {
vm.removeSourceBranch(); vm.removeSourceBranch();
setTimeout(() => { setTimeout(() => {
const args = eventHub.$emit.calls.argsFor(0); const args = eventHub.$emit.calls.argsFor(0);
expect(vm.isMakingRequest).toBeTruthy(); expect(vm.isMakingRequest).toEqual(true);
expect(args[0]).toEqual('MRWidgetUpdateRequested'); expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).not.toThrow(); expect(args[1]).not.toThrow();
done(); done();
...@@ -132,53 +121,50 @@ describe('MRWidgetMerged', () => { ...@@ -132,53 +121,50 @@ describe('MRWidgetMerged', () => {
}); });
}); });
describe('template', () => { it('has merged by information', () => {
let vm; expect(vm.$el.textContent).toContain('Merged by');
let el; expect(vm.$el.textContent).toContain('Administrator');
});
beforeEach(() => { it('renders branch information', () => {
vm = createComponent(); expect(vm.$el.textContent).toContain('The changes were merged into');
el = vm.$el; expect(vm.$el.textContent).toContain(targetBranch);
}); });
it('should have correct elements', () => { it('renders information about branch being removed', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.textContent).toContain('The source branch has been removed');
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) => { it('shows revert and cherry-pick buttons', () => {
vm.mr.sourceBranchRemoved = false; expect(vm.$el.textContent).toContain('Revert');
expect(vm.$el.textContent).toContain('Cherry-pick');
});
Vue.nextTick(() => { it('should not show source branch removed text', (done) => {
expect(el.innerText).toContain('You can remove source branch now'); vm.mr.sourceBranchRemoved = false;
expect(el.innerText).not.toContain('The source branch has been removed');
done(); Vue.nextTick(() => {
}); expect(vm.$el.innerText).toContain('You can remove source branch now');
expect(vm.$el.innerText).not.toContain('The source branch has been removed');
done();
}); });
});
it('should show source branch removing text', (done) => { it('should show source branch removing text', (done) => {
vm.mr.isRemovingSourceBranch = true; vm.mr.isRemovingSourceBranch = true;
vm.mr.sourceBranchRemoved = false; vm.mr.sourceBranchRemoved = false;
Vue.nextTick(() => { Vue.nextTick(() => {
expect(el.innerText).toContain('The source branch is being removed'); expect(vm.$el.innerText).toContain('The source branch is being removed');
expect(el.innerText).not.toContain('You can remove source branch now'); expect(vm.$el.innerText).not.toContain('You can remove source branch now');
expect(el.innerText).not.toContain('The source branch has been removed'); expect(vm.$el.innerText).not.toContain('The source branch has been removed');
done(); done();
});
}); });
});
it('should use mergedEvent updatedAt as tooltip title', () => { it('should use mergedEvent mergedAt as tooltip title', () => {
expect( expect(
el.querySelector('time').getAttribute('title'), vm.$el.querySelector('time').getAttribute('title'),
).toBe('mergedUpdatedAt'); ).toBe('Jan 24, 2018 1:02pm GMT+0000');
});
}); });
}); });
require 'spec_helper'
describe Gitlab::Popen::Runner do
subject { described_class.new }
describe '#run' do
it 'runs the command and returns the result' do
run_command
expect(Gitlab::Popen).to have_received(:popen_with_detail)
end
end
describe '#all_success_and_clean?' do
it 'returns true when exit status is 0 and stderr is empty' do
run_command
expect(subject).to be_all_success_and_clean
end
it 'returns false when exit status is not 0' do
run_command(exitstatus: 1)
expect(subject).not_to be_all_success_and_clean
end
it 'returns false when exit stderr has something' do
run_command(stderr: 'stderr')
expect(subject).not_to be_all_success_and_clean
end
end
describe '#all_success?' do
it 'returns true when exit status is 0' do
run_command
expect(subject).to be_all_success
end
it 'returns false when exit status is not 0' do
run_command(exitstatus: 1)
expect(subject).not_to be_all_success
end
it 'returns true' do
run_command(stderr: 'stderr')
expect(subject).to be_all_success
end
end
describe '#all_stderr_empty?' do
it 'returns true when stderr is empty' do
run_command
expect(subject).to be_all_stderr_empty
end
it 'returns true when exit status is not 0' do
run_command(exitstatus: 1)
expect(subject).to be_all_stderr_empty
end
it 'returns false when exit stderr has something' do
run_command(stderr: 'stderr')
expect(subject).not_to be_all_stderr_empty
end
end
describe '#failed_results' do
it 'returns [] when everything is passed' do
run_command
expect(subject.failed_results).to be_empty
end
it 'returns the result when exit status is not 0' do
result = run_command(exitstatus: 1)
expect(subject.failed_results).to contain_exactly(result)
end
it 'returns [] when exit stderr has something' do
run_command(stderr: 'stderr')
expect(subject.failed_results).to be_empty
end
end
describe '#warned_results' do
it 'returns [] when everything is passed' do
run_command
expect(subject.warned_results).to be_empty
end
it 'returns [] when exit status is not 0' do
run_command(exitstatus: 1)
expect(subject.warned_results).to be_empty
end
it 'returns the result when exit stderr has something' do
result = run_command(stderr: 'stderr')
expect(subject.warned_results).to contain_exactly(result)
end
end
def run_command(
command: 'command',
stdout: 'stdout',
stderr: '',
exitstatus: 0,
status: double(exitstatus: exitstatus, success?: exitstatus.zero?),
duration: 0.1)
result =
Gitlab::Popen::Result.new(command, stdout, stderr, status, duration)
allow(Gitlab::Popen)
.to receive(:popen_with_detail)
.and_return(result)
subject.run([command]) do |cmd, &run|
expect(cmd).to eq(command)
cmd_result = run.call
expect(cmd_result).to eq(result)
end
subject.results.first
end
end
require 'spec_helper' require 'spec_helper'
describe 'Gitlab::Popen' do describe Gitlab::Popen do
let(:path) { Rails.root.join('tmp').to_s } let(:path) { Rails.root.join('tmp').to_s }
before do before do
@klass = Class.new(Object) @klass = Class.new(Object)
@klass.send(:include, Gitlab::Popen) @klass.send(:include, described_class)
end
describe '.popen_with_detail' do
subject { @klass.new.popen_with_detail(cmd) }
let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1);$stderr.puts(2);exit(3)] }
it { expect(subject.cmd).to eq(cmd) }
it { expect(subject.stdout).to eq("1\n") }
it { expect(subject.stderr).to eq("2\n") }
it { expect(subject.status.exitstatus).to eq(3) }
it { expect(subject.duration).to be_kind_of(Numeric) }
end end
context 'zero status' do context 'zero status' do
......
...@@ -277,7 +277,7 @@ describe Ci::Build do ...@@ -277,7 +277,7 @@ describe Ci::Build do
allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1)
end end
it { is_expected.to be_an(Array).and all(include(key: "key:1")) } it { is_expected.to be_an(Array).and all(include(key: "key_1")) }
end end
context 'when project does not have jobs_cache_index' do context 'when project does not have jobs_cache_index' do
......
...@@ -228,7 +228,7 @@ eos ...@@ -228,7 +228,7 @@ eos
it { expect(data).to be_a(Hash) } it { expect(data).to be_a(Hash) }
it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') } it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') } it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') }
it { expect(data[:added]).to eq(["bar/branch-test.txt"]) } it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") }
it { expect(data[:modified]).to eq([]) } it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) } it { expect(data[:removed]).to eq([]) }
end end
...@@ -532,8 +532,8 @@ eos ...@@ -532,8 +532,8 @@ eos
let(:commit2) { merge_request1.merge_request_diff.commits.first } let(:commit2) { merge_request1.merge_request_diff.commits.first }
it 'returns merge_requests that introduced that commit' do it 'returns merge_requests that introduced that commit' do
expect(commit1.merge_requests).to eq([merge_request1, merge_request2]) expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2)
expect(commit2.merge_requests).to eq([merge_request1]) expect(commit2.merge_requests).to contain_exactly(merge_request1)
end end
end end
end end
...@@ -1539,7 +1539,7 @@ describe MergeRequest do ...@@ -1539,7 +1539,7 @@ describe MergeRequest do
expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1) expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
end end
it "executs diff cache service" do it "executes diff cache service" do
expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject) expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
subject.reload_diff subject.reload_diff
......
...@@ -3,6 +3,29 @@ require 'spec_helper' ...@@ -3,6 +3,29 @@ require 'spec_helper'
describe JiraService do describe JiraService do
include Gitlab::Routing include Gitlab::Routing
describe '#options' do
let(:service) do
described_class.new(
project: build_stubbed(:project),
active: true,
username: 'username',
password: 'test',
jira_issue_transition_id: 24,
url: 'http://jira.test.com/path/'
)
end
it 'sets the URL properly' do
# jira-ruby gem parses the URI and handles trailing slashes
# fine: https://github.com/sumoheavy/jira-ruby/blob/v1.4.1/lib/jira/http_client.rb#L59
expect(service.options[:site]).to eq('http://jira.test.com/')
end
it 'leaves out trailing slashes in context' do
expect(service.options[:context_path]).to eq('/path')
end
end
describe "Associations" do describe "Associations" do
it { is_expected.to belong_to :project } it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook } it { is_expected.to have_one :service_hook }
......
...@@ -198,6 +198,8 @@ describe API::MergeRequests do ...@@ -198,6 +198,8 @@ describe API::MergeRequests do
create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
expect do expect do
get api("/projects/#{project.id}/merge_requests", user) get api("/projects/#{project.id}/merge_requests", user)
end.not_to exceed_query_limit(control) end.not_to exceed_query_limit(control)
......
...@@ -55,11 +55,12 @@ describe MergeRequests::RefreshService do ...@@ -55,11 +55,12 @@ describe MergeRequests::RefreshService do
before do before do
allow(refresh_service).to receive(:execute_hooks) allow(refresh_service).to receive(:execute_hooks)
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
end end
it 'executes hooks with update action' do it 'executes hooks with update action' do
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
reload_mrs
expect(refresh_service).to have_received(:execute_hooks) expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev) .with(@merge_request, 'update', old_rev: @oldrev)
...@@ -72,6 +73,26 @@ describe MergeRequests::RefreshService do ...@@ -72,6 +73,26 @@ describe MergeRequests::RefreshService do
expect(@build_failed_todo).to be_done expect(@build_failed_todo).to be_done
expect(@fork_build_failed_todo).to be_done expect(@fork_build_failed_todo).to be_done
end end
context 'when source branch ref does not exists' do
before do
DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
end
it 'closes MRs without source branch ref' do
expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }
.to change { @merge_request.reload.state }
.from('opened')
.to('closed')
expect(@fork_merge_request.reload).to be_open
end
it 'does not change the merge request diff' do
expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }
.not_to change { @merge_request.reload.merge_request_diff }
end
end
end end
context 'when pipeline exists for the source branch' do context 'when pipeline exists for the source branch' do
......
require 'action_dispatch/testing/test_request' require 'action_dispatch/testing/test_request'
require 'fileutils' require 'fileutils'
require 'gitlab/popen'
module JavaScriptFixturesHelpers module JavaScriptFixturesHelpers
include Gitlab::Popen include Gitlab::Popen
......
require 'spec_helper' require 'spec_helper'
require 'tasks/gitlab/task_helpers'
class TestHelpersTest class TestHelpersTest
include Gitlab::TaskHelpers include Gitlab::TaskHelpers
......
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