summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2018-01-30 14:50:17 +0000
committerLin Jen-Shin <godfat@godfat.org>2018-01-30 14:50:17 +0000
commit07557a946bac1b596a28293d39e7bd2581ff828b (patch)
treef725f935880b122862880f9f18d18fb74a7f758b
parentbf01452cc361842dab7442a9ad868213d53bd212 (diff)
parent03f386c2b20f95272e4846f5ecab54fd8b2566e0 (diff)
downloadgitlab-ce-qa-define-selectors.tar.gz
Merge branch 'master' into 'qa-define-selectors'qa-define-selectors
# Conflicts: # qa/qa/page/group/show.rb
-rw-r--r--.gitlab-ci.yml1
-rw-r--r--CHANGELOG.md16
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js15
-rw-r--r--app/assets/javascripts/create_item_dropdown.js44
-rw-r--r--app/assets/javascripts/gl_dropdown.js31
-rw-r--r--app/assets/javascripts/graphs/graphs_show.js16
-rw-r--r--app/assets/javascripts/group_label_subscription.js27
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js48
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js13
-rw-r--r--app/assets/javascripts/issuable_index.js34
-rw-r--r--app/assets/javascripts/label_manager.js32
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue11
-rw-r--r--app/assets/javascripts/single_file_diff.js26
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js139
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue192
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/dependencies.js2
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/services/merge_requests/refresh_service.rb16
-rw-r--r--app/views/layouts/header/_new_dropdown.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rwxr-xr-xbin/upgrade.rb3
-rw-r--r--changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml5
-rw-r--r--changelogs/unreleased/41802-add-space-to-edit-delete-tag-btns.yml5
-rw-r--r--changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml5
-rw-r--r--changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml5
-rw-r--r--changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml6
-rw-r--r--changelogs/unreleased/fix-postgresql-table-grant.yml5
-rw-r--r--changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml5
-rw-r--r--changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml5
-rw-r--r--changelogs/unreleased/sh-fix-jira-trailing-slash.yml5
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/grape_route_helpers_fix.rb16
-rw-r--r--doc/api/repository_files.md13
-rw-r--r--doc/user/project/repository/branches/index.md2
-rw-r--r--lib/api/entities.rb10
-rw-r--r--lib/gitlab/popen.rb27
-rw-r--r--lib/gitlab/popen/runner.rb46
-rw-r--r--lib/gitlab/redis/cache.rb2
-rw-r--r--lib/gitlab/task_helpers.rb (renamed from lib/tasks/gitlab/task_helpers.rb)9
-rw-r--r--lib/gitlab/upgrader.rb3
-rw-r--r--lib/system_check/helpers.rb2
-rw-r--r--lib/tasks/flay.rake2
-rw-r--r--lib/tasks/gitlab/backup.rake36
-rw-r--r--lib/tasks/gitlab/check.rake24
-rw-r--r--lib/tasks/gitlab/cleanup.rake8
-rw-r--r--lib/tasks/gitlab/git.rake8
-rw-r--r--lib/tasks/gitlab/gitaly.rake2
-rw-r--r--lib/tasks/gitlab/helpers.rake4
-rw-r--r--lib/tasks/gitlab/info.rake2
-rw-r--r--lib/tasks/gitlab/setup.rake2
-rw-r--r--lib/tasks/gitlab/shell.rake8
-rw-r--r--lib/tasks/gitlab/workhorse.rake2
-rw-r--r--lib/tasks/haml-lint.rake9
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake20
-rw-r--r--qa/qa.rb7
-rw-r--r--qa/qa/factory/resource/issue.rb34
-rw-r--r--qa/qa/page/base.rb32
-rw-r--r--qa/qa/page/group/show.rb25
-rw-r--r--qa/qa/page/menu/side.rb8
-rw-r--r--qa/qa/page/project/issue/index.rb17
-rw-r--r--qa/qa/page/project/issue/new.rb33
-rw-r--r--qa/qa/page/project/issue/show.rb37
-rw-r--r--qa/qa/page/project/show.rb11
-rw-r--r--qa/spec/fixtures/banana_sample.gifbin0 -> 71759 bytes
-rwxr-xr-xscripts/static-analysis57
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb7
-rw-r--r--spec/features/projects/blobs/edit_spec.rb22
-rw-r--r--spec/initializers/grape_route_helpers_fix_spec.rb14
-rw-r--r--spec/javascripts/create_item_dropdown_spec.js139
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js128
-rw-r--r--spec/javascripts/issuable_spec.js26
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js208
-rw-r--r--spec/lib/gitlab/popen/runner_spec.rb139
-rw-r--r--spec/lib/gitlab/popen_spec.rb16
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/commit_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb23
-rw-r--r--spec/requests/api/merge_requests_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb25
-rw-r--r--spec/support/javascript_fixtures_helpers.rb1
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb1
86 files changed, 1351 insertions, 661 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index c1d78ef2d48..be18520b876 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -321,6 +321,7 @@ setup-test-env:
expire_in: 7d
paths:
- tmp/tests
+ - config/secrets.yml
rspec-pg 0 27: *rspec-metadata-pg
rspec-pg 1 27: *rspec-metadata-pg
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 248c85304a9..5fc97c06f7c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,22 @@
documentation](doc/development/changelog.md) for instructions on adding your own
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)
### Fixed (4 changes)
diff --git a/Gemfile b/Gemfile
index ee576c53fe9..05f72b6482f 100644
--- a/Gemfile
+++ b/Gemfile
@@ -325,7 +325,7 @@ group :development, :test do
gem 'spinach-rerun-reporter', '~> 0.0.2'
gem 'rspec_profiling', '~> 0.0.5'
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)
gem 'minitest', '~> 5.7.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5532888d179..1a3c8f42469 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -304,7 +304,7 @@ GEM
mime-types (>= 1.16)
posix-spawn (~> 0.3)
gitlab-markup (1.6.3)
- gitlab-styles (2.3.1)
+ gitlab-styles (2.3.2)
rubocop (~> 0.51)
rubocop-gitlab-security (~> 0.1.0)
rubocop-rspec (~> 1.19)
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index b37988a674d..a25f7fb3dcd 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,5 +1,8 @@
/* global ace */
+import axios from '~/lib/utils/axios_utils';
+import createFlash from '~/flash';
+import { __ } from '~/locale';
import TemplateSelectorMediator from '../blob/file_template_mediator';
export default class EditBlob {
@@ -56,12 +59,14 @@ export default class EditBlob {
if (paneId === '#preview') {
this.$toggleButton.hide();
- return $.post(currentLink.data('preview-url'), {
+ axios.post(currentLink.data('preview-url'), {
content: this.editor.getValue(),
- }, (response) => {
- currentPane.empty().append(response);
- return currentPane.renderGFM();
- });
+ })
+ .then(({ data }) => {
+ currentPane.empty().append(data);
+ currentPane.renderGFM();
+ })
+ .catch(() => createFlash(__('An error occurred previewing the blob')));
}
this.$toggleButton.show();
diff --git a/app/assets/javascripts/create_item_dropdown.js b/app/assets/javascripts/create_item_dropdown.js
index 488db023ee7..42e9e568170 100644
--- a/app/assets/javascripts/create_item_dropdown.js
+++ b/app/assets/javascripts/create_item_dropdown.js
@@ -12,6 +12,7 @@ export default class CreateItemDropdown {
this.fieldName = options.fieldName;
this.onSelect = options.onSelect || (() => {});
this.getDataOption = options.getData;
+ this.createNewItemFromValueOption = options.createNewItemFromValue;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
@@ -30,15 +31,15 @@ export default class CreateItemDropdown {
filterable: true,
remote: false,
search: {
- fields: ['title'],
+ fields: ['text'],
},
selectable: true,
toggleLabel(selected) {
- return (selected && 'id' in selected) ? selected.title : this.defaultToggleLabel;
+ return (selected && 'id' in selected) ? _.escape(selected.title) : this.defaultToggleLabel;
},
fieldName: this.fieldName,
text(item) {
- return _.escape(item.title);
+ return _.escape(item.text);
},
id(item) {
return _.escape(item.id);
@@ -51,6 +52,11 @@ export default class CreateItemDropdown {
});
}
+ clearDropdown() {
+ this.$dropdownContainer.find('.dropdown-content').html('');
+ this.$dropdownContainer.find('.dropdown-input-field').val('');
+ }
+
bindEvents() {
this.$createButton.on('click', this.onClickCreateWildcard.bind(this));
}
@@ -58,9 +64,13 @@ export default class CreateItemDropdown {
onClickCreateWildcard(e) {
e.preventDefault();
+ this.refreshData();
+ this.$dropdown.data('glDropdown').selectRowAtIndex();
+ }
+
+ refreshData() {
// Refresh the dropdown's data, which ends up calling `getData`
this.$dropdown.data('glDropdown').remote.execute();
- this.$dropdown.data('glDropdown').selectRowAtIndex();
}
getData(term, callback) {
@@ -79,20 +89,28 @@ export default class CreateItemDropdown {
});
}
- toggleCreateNewButton(item) {
- if (item) {
- this.selectedItem = {
- title: item,
- id: item,
- text: item,
- };
+ createNewItemFromValue(newValue) {
+ if (this.createNewItemFromValueOption) {
+ return this.createNewItemFromValueOption(newValue);
+ }
+
+ return {
+ title: newValue,
+ id: newValue,
+ text: newValue,
+ };
+ }
+
+ toggleCreateNewButton(newValue) {
+ if (newValue) {
+ this.selectedItem = this.createNewItemFromValue(newValue);
this.$dropdownContainer
.find('.js-dropdown-create-new-item code')
- .text(item);
+ .text(newValue);
}
- this.toggleFooter(!item);
+ this.toggleFooter(!newValue);
}
toggleFooter(toggleState) {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 64f258aed64..15df7a7f989 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -2,6 +2,7 @@
/* global fuzzaldrinPlus */
import _ from 'underscore';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import { isObject } from './lib/utils/type_utility';
@@ -212,25 +213,17 @@ GitLabDropdownRemote = (function() {
};
GitLabDropdownRemote.prototype.fetchData = function() {
- return $.ajax({
- url: this.dataEndpoint,
- dataType: this.options.dataType,
- beforeSend: (function(_this) {
- return function() {
- if (_this.options.beforeSend) {
- return _this.options.beforeSend();
- }
- };
- })(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
+ if (this.options.beforeSend) {
+ this.options.beforeSend();
+ }
+
+ // Fetch the data through ajax if the data is a string
+ return axios.get(this.dataEndpoint)
+ .then(({ data }) => {
+ if (this.options.success) {
+ return this.options.success(data);
+ }
+ });
};
return GitLabDropdownRemote;
diff --git a/app/assets/javascripts/graphs/graphs_show.js b/app/assets/javascripts/graphs/graphs_show.js
index 36bad6db3e1..b670e907a5c 100644
--- a/app/assets/javascripts/graphs/graphs_show.js
+++ b/app/assets/javascripts/graphs/graphs_show.js
@@ -1,11 +1,13 @@
+import flash from '../flash';
+import { __ } from '../locale';
+import axios from '../lib/utils/axios_utils';
import ContributorsStatGraph from './stat_graph_contributors';
document.addEventListener('DOMContentLoaded', () => {
- $.ajax({
- type: 'GET',
- url: document.querySelector('.js-graphs-show').dataset.projectGraphPath,
- dataType: 'json',
- success(data) {
+ const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
+
+ axios.get(url)
+ .then(({ data }) => {
const graph = new ContributorsStatGraph();
graph.init(data);
@@ -16,6 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
$('.stat-graph').fadeIn();
$('.loading-graph').hide();
- },
- });
+ })
+ .catch(() => flash(__('Error fetching contributors data.')));
});
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index befaebb635e..df9429b1e02 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,3 +1,7 @@
+import axios from './lib/utils/axios_utils';
+import flash from './flash';
+import { __ } from './locale';
+
export default class GroupLabelSubscription {
constructor(container) {
const $container = $(container);
@@ -13,14 +17,12 @@ export default class GroupLabelSubscription {
event.preventDefault();
const url = this.$unsubscribeButtons.attr('data-url');
-
- $.ajax({
- type: 'POST',
- url,
- }).done(() => {
- this.toggleSubscriptionButtons();
- this.$unsubscribeButtons.removeAttr('data-url');
- });
+ axios.post(url)
+ .then(() => {
+ this.toggleSubscriptionButtons();
+ this.$unsubscribeButtons.removeAttr('data-url');
+ })
+ .catch(() => flash(__('There was an error when unsubscribing from this label.')));
}
subscribe(event) {
@@ -31,12 +33,9 @@ export default class GroupLabelSubscription {
this.$unsubscribeButtons.attr('data-url', url);
- $.ajax({
- type: 'POST',
- url,
- }).done(() => {
- this.toggleSubscriptionButtons();
- });
+ axios.post(url)
+ .then(() => this.toggleSubscriptionButtons())
+ .catch(() => flash(__('There was an error when subscribing to this label.')));
}
toggleSubscriptionButtons() {
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
index 32415a8791f..3f27cfc2f6d 100644
--- a/app/assets/javascripts/integrations/integration_settings_form.js
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -1,4 +1,5 @@
-import Flash from '../flash';
+import axios from '../lib/utils/axios_utils';
+import flash from '../flash';
export default class IntegrationSettingsForm {
constructor(formSelector) {
@@ -95,29 +96,26 @@ export default class IntegrationSettingsForm {
*/
testSettings(formData) {
this.toggleSubmitBtnState(true);
- $.ajax({
- type: 'PUT',
- url: this.testEndPoint,
- data: formData,
- })
- .done((res) => {
- if (res.error) {
- new Flash(`${res.message} ${res.service_response}`, 'alert', document, {
- title: 'Save anyway',
- clickHandler: (e) => {
- e.preventDefault();
- this.$form.submit();
- },
- });
- } else {
- this.$form.submit();
- }
- })
- .fail(() => {
- new Flash('Something went wrong on our end.');
- })
- .always(() => {
- this.toggleSubmitBtnState(false);
- });
+
+ return axios.put(this.testEndPoint, formData)
+ .then(({ data }) => {
+ if (data.error) {
+ flash(`${data.message} ${data.service_response}`, 'alert', document, {
+ title: 'Save anyway',
+ clickHandler: (e) => {
+ e.preventDefault();
+ this.$form.submit();
+ },
+ });
+ } else {
+ this.$form.submit();
+ }
+
+ this.toggleSubmitBtnState(false);
+ })
+ .catch(() => {
+ flash('Something went wrong on our end.');
+ this.toggleSubmitBtnState(false);
+ });
}
}
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index b124fafec70..8c1b2e78ca4 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -1,5 +1,6 @@
/* 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 axios from './lib/utils/axios_utils';
import Flash from './flash';
export default {
@@ -22,15 +23,9 @@ export default {
},
submit() {
- const _this = this;
- const xhr = $.ajax({
- url: this.form.attr('action'),
- method: this.form.attr('method'),
- dataType: 'JSON',
- data: this.getFormDataAsObject()
- });
- xhr.done(() => window.location.reload());
- xhr.fail(() => this.onFormSubmitFailure());
+ axios[this.form.attr('method')](this.form.attr('action'), this.getFormDataAsObject())
+ .then(() => window.location.reload())
+ .catch(() => this.onFormSubmitFailure());
},
onFormSubmitFailure() {
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index c3e0acdff66..0683ca82a38 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,3 +1,6 @@
+import axios from './lib/utils/axios_utils';
+import flash from './flash';
+import { __ } from './locale';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
@@ -20,23 +23,24 @@ export default class IssuableIndex {
}
static resetIncomingEmailToken() {
- $('.incoming-email-token-reset').on('click', (e) => {
+ const $resetToken = $('.incoming-email-token-reset');
+
+ $resetToken.on('click', (e) => {
e.preventDefault();
- $.ajax({
- type: 'PUT',
- url: $('.incoming-email-token-reset').attr('href'),
- dataType: 'json',
- success(response) {
- $('#issuable_email').val(response.new_address).focus();
- },
- beforeSend() {
- $('.incoming-email-token-reset').text('resetting...');
- },
- complete() {
- $('.incoming-email-token-reset').text('reset it');
- },
- });
+ $resetToken.text('resetting...');
+
+ axios.put($resetToken.attr('href'))
+ .then(({ data }) => {
+ $('#issuable_email').val(data.new_address).focus();
+
+ $resetToken.text('reset it');
+ })
+ .catch(() => {
+ flash(__('There was an error when reseting email token.'));
+
+ $resetToken.text('reset it');
+ });
});
}
}
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index ac2f636df0f..61b40f79db1 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -1,7 +1,8 @@
/* 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 Flash from './flash';
+import flash from './flash';
+import axios from './lib/utils/axios_utils';
export default class LabelManager {
constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
@@ -50,11 +51,12 @@ export default class LabelManager {
if (persistState == null) {
persistState = true;
}
- let xhr;
const _this = this;
const url = $label.find('.js-toggle-priority').data('url');
let $target = this.prioritizedLabels;
let $from = this.otherLabels;
+ const rollbackLabelPosition = this.rollbackLabelPosition.bind(this, $label, action);
+
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
@@ -71,40 +73,34 @@ export default class LabelManager {
return;
}
if (action === 'remove') {
- xhr = $.ajax({
- url,
- type: 'DELETE'
- });
+ axios.delete(url)
+ .catch(rollbackLabelPosition);
+
// Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
} else {
- xhr = this.savePrioritySort($label, action);
+ this.savePrioritySort($label, action)
+ .catch(rollbackLabelPosition);
}
- return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
}
onPrioritySortUpdate() {
- const xhr = this.savePrioritySort();
- return xhr.fail(function() {
- return new Flash(this.errorMessage, 'alert');
- });
+ this.savePrioritySort()
+ .catch(() => flash(this.errorMessage));
}
savePrioritySort() {
- return $.post({
- url: this.prioritizedLabels.data('url'),
- data: {
- label_ids: this.getSortedLabelsIds()
- }
+ return axios.post(this.prioritizedLabels.data('url'), {
+ label_ids: this.getSortedLabelsIds(),
});
}
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
- return new Flash(this.errorMessage, 'alert');
+ flash(this.errorMessage);
}
getSortedLabelsIds() {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 025e38ea99a..5afae93724b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -76,7 +76,13 @@
.then(data => this.store.storeDeploymentData(data))
.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'; });
},
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 87d1975d5ad..56cd60c583b 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -34,16 +34,23 @@
svgUrl: this.emptyGettingStartedSvgPath,
title: 'Get started with performance monitoring',
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',
},
loading: {
svgUrl: this.emptyLoadingSvgPath,
title: 'Waiting for performance data',
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',
},
+ 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: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: 'Unable to connect to Prometheus server',
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 95e51bc4e7a..48dd91bdf06 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,5 +1,8 @@
/* 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 imageDiffHelper from './image_diff/helpers/index';
import syntaxHighlight from './syntax_highlight';
@@ -60,30 +63,31 @@ export default class SingleFileDiff {
getContentHTML(cb) {
this.collapsedContent.hide();
this.loadingContent.show();
- $.get(this.diffForPath, (function(_this) {
- return function(data) {
- _this.loadingContent.hide();
+
+ axios.get(this.diffForPath)
+ .then(({ data }) => {
+ this.loadingContent.hide();
if (data.html) {
- _this.content = $(data.html);
- syntaxHighlight(_this.content);
+ this.content = $(data.html);
+ syntaxHighlight(this.content);
} else {
- _this.hasError = true;
- _this.content = $(ERROR_HTML);
+ this.hasError = true;
+ this.content = $(ERROR_HTML);
}
- _this.collapsedContent.after(_this.content);
+ this.collapsedContent.after(this.content);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
- const $file = $(_this.file);
+ const $file = $(this.file);
FilesCommentButton.init($file);
const canCreateNote = $file.closest('.files').is('[data-can-create-note]');
imageDiffHelper.initImageDiff($file[0], canCreateNote);
if (cb) cb();
- };
- })(this));
+ })
+ .catch(createFlash(__('An error occurred while retrieving diff')));
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
deleted file mode 100644
index 7f8d78cab73..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
+++ /dev/null
@@ -1,139 +0,0 @@
-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>
- `,
-};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
new file mode 100644
index 00000000000..a92e0b3c124
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -0,0 +1,192 @@
+<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>
diff --git a/app/assets/javascripts/vue_merge_request_widget/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
index b930aca6877..2917090e073 100644
--- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js
+++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js
@@ -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 WidgetDeployment } from './components/mr_widget_deployment';
export { default as WidgetRelatedLinks } from './components/mr_widget_related_links';
-export { default as MergedState } from './components/states/mr_widget_merged';
+export { default as MergedState } from './components/states/mr_widget_merged.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 MergingState } from './components/states/mr_widget_merging.vue';
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index df67fb243ad..6ced5fb0e24 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -466,7 +466,7 @@ module Ci
if cache && project.jobs_cache_index
cache = cache.merge(
- key: "#{cache[:key]}:#{project.jobs_cache_index}")
+ key: "#{cache[:key]}_#{project.jobs_cache_index}")
end
[cache]
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 2be35b6ea9d..23147d7f666 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -43,7 +43,7 @@ class JiraService < IssueTrackerService
username: self.username,
password: self.password,
site: URI.join(url, '/').to_s,
- context_path: url.path,
+ context_path: url.path.chomp('/'),
auth_type: :basic,
read_timeout: 120,
use_cookies: true,
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 9f05535d4d4..262622f8bd0 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -9,7 +9,8 @@ module MergeRequests
Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits))
# Be sure to close outstanding MRs before reloading them to avoid generating an
# empty diff during a manual merge
- close_merge_requests
+ close_upon_missing_source_branch_ref
+ post_merge_manually_merged
reload_merge_requests
reset_merge_when_pipeline_succeeds
mark_pending_todos_done
@@ -29,11 +30,22 @@ module MergeRequests
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
# 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
# target branch manually
- def close_merge_requests
+ def post_merge_manually_merged
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 = merge_requests.select(&:diff_head_commit)
diff --git a/app/views/layouts/header/_new_dropdown.haml b/app/views/layouts/header/_new_dropdown.haml
index 088f2785092..eb32f393310 100644
--- a/app/views/layouts/header/_new_dropdown.haml
+++ b/app/views/layouts/header/_new_dropdown.haml
@@ -1,5 +1,5 @@
%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('angle-down', css_class: 'caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 467f19b4c56..55e45a5e954 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -32,5 +32,5 @@
= icon("pencil")
- 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")
diff --git a/bin/upgrade.rb b/bin/upgrade.rb
deleted file mode 100755
index a5caecf8526..00000000000
--- a/bin/upgrade.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-require_relative "../lib/gitlab/upgrader"
-
-Gitlab::Upgrader.new.execute
diff --git a/changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml b/changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml
deleted file mode 100644
index f4c44983736..00000000000
--- a/changelogs/unreleased/32546-cannot-copy-paste-on-ios.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix copy/paste on iOS devices due to a bug in webkit
-merge_request: 15804
-author:
-type: fixed
diff --git a/changelogs/unreleased/41802-add-space-to-edit-delete-tag-btns.yml b/changelogs/unreleased/41802-add-space-to-edit-delete-tag-btns.yml
new file mode 100644
index 00000000000..f23a6452b0d
--- /dev/null
+++ b/changelogs/unreleased/41802-add-space-to-edit-delete-tag-btns.yml
@@ -0,0 +1,5 @@
+---
+title: Adds spacing between edit and delete tag btn in tag list
+merge_request: 16757
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml b/changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml
deleted file mode 100644
index 38684cd3c44..00000000000
--- a/changelogs/unreleased/42022-allow-users-to-request-access-not-visible-when-project-visibility-is-public.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix missing "allow users to request access" option in public project permissions
-merge_request: 16485
-author:
-type: fixed
diff --git a/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml b/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml
deleted file mode 100644
index c64bee9126e..00000000000
--- a/changelogs/unreleased/42161-gitaly-commitservice-encoding-undefinedconversionerror-u-c124-from-utf-8-to-ascii-8bit.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix encoding issue when counting commit count
-merge_request: 16637
-author:
-type: fixed
diff --git a/changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml b/changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml
deleted file mode 100644
index 660f4f5d42c..00000000000
--- a/changelogs/unreleased/42327-import-from-gitlab-com-fails-destination-already-exists-and-is-not-an-empty-directory-error.yml
+++ /dev/null
@@ -1,6 +0,0 @@
----
-title: Fixes destination already exists, and some particular service errors on Import/Export
- error
-merge_request: 16714
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-postgresql-table-grant.yml b/changelogs/unreleased/fix-postgresql-table-grant.yml
deleted file mode 100644
index 1c6559f6f73..00000000000
--- a/changelogs/unreleased/fix-postgresql-table-grant.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Use has_table_privilege for TRIGGER on PostgreSQL
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml b/changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml
new file mode 100644
index 00000000000..1cffb213f23
--- /dev/null
+++ b/changelogs/unreleased/osw-fix-lost-diffs-when-source-branch-deleted.yml
@@ -0,0 +1,5 @@
+---
+title: Close and do not reload MR diffs when source branch is deleted
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml
new file mode 100644
index 00000000000..3854985e576
--- /dev/null
+++ b/changelogs/unreleased/osw-updates-merge-status-on-api-actions.yml
@@ -0,0 +1,5 @@
+---
+title: Return more consistent values for merge_status on MR APIs
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-jira-trailing-slash.yml b/changelogs/unreleased/sh-fix-jira-trailing-slash.yml
new file mode 100644
index 00000000000..786f6cd3727
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-jira-trailing-slash.yml
@@ -0,0 +1,5 @@
+---
+title: Fix JIRA not working when a trailing slash is included
+merge_request:
+author:
+type: fixed
diff --git a/config/application.rb b/config/application.rb
index 2067428ff62..751307de975 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -6,6 +6,7 @@ Bundler.require(:default, Rails.env)
module Gitlab
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/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb
index d3cf9e453d0..612cca3dfbd 100644
--- a/config/initializers/grape_route_helpers_fix.rb
+++ b/config/initializers/grape_route_helpers_fix.rb
@@ -1,5 +1,21 @@
if defined?(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
# GrapeRouteHelpers gem tries to parse the versions
# from a string, not supporting Grape `version` array definition.
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index a1a0b1b756c..c29dc22e12d 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -68,7 +68,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch": "master"
}
```
@@ -98,7 +98,7 @@ Example response:
```json
{
- "file_name": "app/project.rb",
+ "file_path": "app/project.rb",
"branch": "master"
}
```
@@ -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'
```
-Example response:
-
-```json
-{
- "file_name": "app/project.rb",
- "branch": "master"
-}
-```
-
Parameters:
- `file_path` (required) - Url encoded full path to new file. Ex. lib%2Fclass%2Erb
diff --git a/doc/user/project/repository/branches/index.md b/doc/user/project/repository/branches/index.md
index 26c55891b3c..9d16a4c74f2 100644
--- a/doc/user/project/repository/branches/index.md
+++ b/doc/user/project/repository/branches/index.md
@@ -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
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),
which means that _an issue will be closed when a merge request is merged to
the **default branch**_.
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 7b9a80a234b..cb222697f32 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -507,7 +507,15 @@ module API
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
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 :merge_commit_sha
expose :user_notes_count
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index 4bc5cda8cb5..b9832a724c4 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -5,7 +5,17 @@ module Gitlab
module Popen
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)
raise "System commands must be given as an array of strings"
end
@@ -18,18 +28,21 @@ module Gitlab
FileUtils.mkdir_p(path)
end
- cmd_output = ""
- cmd_status = 0
+ cmd_stdout = ''
+ cmd_stderr = ''
+ cmd_status = nil
+ start = Time.now
+
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
yield(stdin) if block_given?
stdin.close
- cmd_output << stdout.read
- cmd_output << stderr.read
- cmd_status = wait_thr.value.exitstatus
+ cmd_stdout = stdout.read
+ cmd_stderr = stderr.read
+ cmd_status = wait_thr.value
end
- [cmd_output, cmd_status]
+ Result.new(cmd, cmd_stdout, cmd_stderr, cmd_status, Time.now - start)
end
end
end
diff --git a/lib/gitlab/popen/runner.rb b/lib/gitlab/popen/runner.rb
new file mode 100644
index 00000000000..f44035a48bb
--- /dev/null
+++ b/lib/gitlab/popen/runner.rb
@@ -0,0 +1,46 @@
+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
diff --git a/lib/gitlab/redis/cache.rb b/lib/gitlab/redis/cache.rb
index 9bf019b72e6..a991933e910 100644
--- a/lib/gitlab/redis/cache.rb
+++ b/lib/gitlab/redis/cache.rb
@@ -1,5 +1,5 @@
# 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 Redis
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/gitlab/task_helpers.rb
index c1182af1014..34bee6fecbe 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/gitlab/task_helpers.rb
@@ -1,6 +1,7 @@
require 'rainbow/ext/string'
require 'gitlab/utils/strong_memoize'
+# rubocop:disable Rails/Output
module Gitlab
TaskFailedError = Class.new(StandardError)
TaskAbortedByUserError = Class.new(StandardError)
@@ -96,11 +97,9 @@ module Gitlab
end
def gid_for(group_name)
- begin
- Etc.getgrnam(group_name).gid
- rescue ArgumentError # no group
- "group #{group_name} doesn't exist"
- end
+ Etc.getgrnam(group_name).gid
+ rescue ArgumentError # no group
+ "group #{group_name} doesn't exist"
end
def gitlab_user
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index 3b64cb32afa..d545f2f95f1 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -1,6 +1,3 @@
-require_relative "popen"
-require_relative "version_info"
-
module Gitlab
class Upgrader
def execute
diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb
index c42ae4fe4c4..914ed794601 100644
--- a/lib/system_check/helpers.rb
+++ b/lib/system_check/helpers.rb
@@ -1,5 +1,3 @@
-require 'tasks/gitlab/task_helpers'
-
module SystemCheck
module Helpers
include ::Gitlab::TaskHelpers
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
index 7ad2b2e4d39..b1e012e70c5 100644
--- a/lib/tasks/flay.rake
+++ b/lib/tasks/flay.rake
@@ -1,6 +1,6 @@
desc 'Code duplication analyze via flay'
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"
puts output
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 2383bcf954b..24e37f6c6cc 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -4,7 +4,7 @@ namespace :gitlab do
namespace :backup do
# Create backup of 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
configure_cron_mode
@@ -25,7 +25,7 @@ namespace :gitlab do
# Restore backup of GitLab system
desc 'GitLab | Restore a previously created backup'
- task restore: :environment do
+ task restore: :gitlab_environment do
warn_user_is_not_gitlab
configure_cron_mode
@@ -73,7 +73,7 @@ namespace :gitlab do
end
namespace :repo do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping repositories ...".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
@@ -84,7 +84,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring repositories ...".color(:blue)
Backup::Repository.new.restore
$progress.puts "done".color(:green)
@@ -92,7 +92,7 @@ namespace :gitlab do
end
namespace :db do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping database ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("db")
@@ -103,7 +103,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring database ... ".color(:blue)
Backup::Database.new.restore
$progress.puts "done".color(:green)
@@ -111,7 +111,7 @@ namespace :gitlab do
end
namespace :builds do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping builds ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("builds")
@@ -122,7 +122,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring builds ... ".color(:blue)
Backup::Builds.new.restore
$progress.puts "done".color(:green)
@@ -130,7 +130,7 @@ namespace :gitlab do
end
namespace :uploads do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping uploads ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
@@ -141,7 +141,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring uploads ... ".color(:blue)
Backup::Uploads.new.restore
$progress.puts "done".color(:green)
@@ -149,7 +149,7 @@ namespace :gitlab do
end
namespace :artifacts do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping artifacts ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
@@ -160,7 +160,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring artifacts ... ".color(:blue)
Backup::Artifacts.new.restore
$progress.puts "done".color(:green)
@@ -168,7 +168,7 @@ namespace :gitlab do
end
namespace :pages do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping pages ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("pages")
@@ -179,7 +179,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring pages ... ".color(:blue)
Backup::Pages.new.restore
$progress.puts "done".color(:green)
@@ -187,7 +187,7 @@ namespace :gitlab do
end
namespace :lfs do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping lfs objects ... ".color(:blue)
if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
@@ -198,7 +198,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring lfs objects ... ".color(:blue)
Backup::Lfs.new.restore
$progress.puts "done".color(:green)
@@ -206,7 +206,7 @@ namespace :gitlab do
end
namespace :registry do
- task create: :environment do
+ task create: :gitlab_environment do
$progress.puts "Dumping container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
@@ -221,7 +221,7 @@ namespace :gitlab do
end
end
- task restore: :environment do
+ task restore: :gitlab_environment do
$progress.puts "Restoring container registry images ... ".color(:blue)
if Gitlab.config.registry.enabled
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index a584eb97cf5..e05a3aad824 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -1,7 +1,3 @@
-# Temporary hack, until we migrate all checks to SystemCheck format
-require 'system_check'
-require 'system_check/helpers'
-
namespace :gitlab do
desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check
@@ -12,7 +8,7 @@ namespace :gitlab do
namespace :app do
desc 'GitLab | Check the configuration of the GitLab Rails app'
- task check: :environment do
+ task check: :gitlab_environment do
warn_user_is_not_gitlab
checks = [
@@ -43,7 +39,7 @@ namespace :gitlab do
namespace :gitlab_shell do
desc "GitLab | Check the configuration of GitLab Shell"
- task check: :environment do
+ task check: :gitlab_environment do
warn_user_is_not_gitlab
start_checking "GitLab Shell"
@@ -251,7 +247,7 @@ namespace :gitlab do
namespace :sidekiq do
desc "GitLab | Check the configuration of Sidekiq"
- task check: :environment do
+ task check: :gitlab_environment do
warn_user_is_not_gitlab
start_checking "Sidekiq"
@@ -310,7 +306,7 @@ namespace :gitlab do
namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
- task check: :environment do
+ task check: :gitlab_environment do
warn_user_is_not_gitlab
if Gitlab.config.incoming_email.enabled
@@ -333,7 +329,7 @@ namespace :gitlab do
end
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.
# This setting only affects the `rake gitlab:check` script.
args.with_defaults(limit: 100)
@@ -389,7 +385,7 @@ namespace :gitlab do
namespace :repo do
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)
Rake::Task["gitlab:git:fsck"].execute
end
@@ -397,7 +393,7 @@ namespace :gitlab do
namespace :orphans do
desc 'Gitlab | Check for orphaned namespaces and repositories'
- task check: :environment do
+ task check: :gitlab_environment do
warn_user_is_not_gitlab
checks = [
SystemCheck::Orphans::NamespaceCheck,
@@ -408,7 +404,7 @@ namespace :gitlab do
end
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
checks = [SystemCheck::Orphans::NamespaceCheck]
@@ -416,7 +412,7 @@ namespace :gitlab do
end
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
checks = [SystemCheck::Orphans::RepositoryCheck]
@@ -426,7 +422,7 @@ namespace :gitlab do
namespace :user do
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))
user = User.find_by(username: username)
if user
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index ab601b0d66b..5a53eac0897 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -5,7 +5,7 @@ namespace :gitlab do
HASHED_REPOSITORY_NAME = '@hashed'.freeze
desc "GitLab | Cleanup | Clean namespaces"
- task dirs: :environment do
+ task dirs: :gitlab_environment do
warn_user_is_not_gitlab
remove_flag = ENV['REMOVE']
@@ -49,7 +49,7 @@ namespace :gitlab do
end
desc "GitLab | Cleanup | Clean repositories"
- task repos: :environment do
+ task repos: :gitlab_environment do
warn_user_is_not_gitlab
move_suffix = "+orphaned+#{Time.now.to_i}"
@@ -78,7 +78,7 @@ namespace :gitlab do
end
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
block_flag = ENV['BLOCK']
@@ -109,7 +109,7 @@ namespace :gitlab do
# 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.
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.find_each do |project|
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
index 3f5dd2ae3b3..cb4f7e5c8a8 100644
--- a/lib/tasks/gitlab/git.rake
+++ b/lib/tasks/gitlab/git.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :git do
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")
if failures.empty?
puts "Done".color(:green)
@@ -11,7 +11,7 @@ namespace :gitlab do
end
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")
if failures.empty?
puts "Done".color(:green)
@@ -21,7 +21,7 @@ namespace :gitlab do
end
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")
if failures.empty?
puts "Done".color(:green)
@@ -31,7 +31,7 @@ namespace :gitlab do
end
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|
check_config_lock(repo)
check_ref_locks(repo)
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index aece8893974..107ff1d8aeb 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :gitaly do
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'
warn_user_is_not_gitlab
diff --git a/lib/tasks/gitlab/helpers.rake b/lib/tasks/gitlab/helpers.rake
index b0a24790c4a..14d1125a03d 100644
--- a/lib/tasks/gitlab/helpers.rake
+++ b/lib/tasks/gitlab/helpers.rake
@@ -1,8 +1,6 @@
-require 'tasks/gitlab/task_helpers'
-
# Prevent StateMachine warnings from outputting during a cron task
StateMachines::Machine.ignore_method_conflicts = true if ENV['CRON']
-namespace :gitlab do
+task gitlab_environment: :environment do
extend SystemCheck::Helpers
end
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index e9fb6a008b0..45e9a1a1c72 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :env do
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
rvm_version = run_and_match(%w(rvm --version), /[\d\.]+/).try(:to_s)
# check Ruby version
diff --git a/lib/tasks/gitlab/setup.rake b/lib/tasks/gitlab/setup.rake
index 05fcb8e3da5..1d903c81358 100644
--- a/lib/tasks/gitlab/setup.rake
+++ b/lib/tasks/gitlab/setup.rake
@@ -1,6 +1,6 @@
namespace :gitlab do
desc "GitLab | Setup production application"
- task setup: :environment do
+ task setup: :gitlab_environment do
setup_db
end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 12ae4199b69..844664b12d4 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :shell do
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
default_version = Gitlab::Shell.version_required
@@ -58,12 +58,12 @@ namespace :gitlab do
end
desc "GitLab | Setup gitlab-shell"
- task setup: :environment do
+ task setup: :gitlab_environment do
setup
end
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|
path_to_repo = project.repository.path_to_repo
if File.exist?(path_to_repo)
@@ -80,7 +80,7 @@ namespace :gitlab do
end
desc 'Create or repair repository hooks symlink'
- task create_hooks: :environment do
+ task create_hooks: :gitlab_environment do
warn_user_is_not_gitlab
puts 'Creating/Repairing hooks symlinks for all repositories'
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index 308ffb0e284..b917a293095 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -1,7 +1,7 @@
namespace :gitlab do
namespace :workhorse do
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
unless args.dir.present?
diff --git a/lib/tasks/haml-lint.rake b/lib/tasks/haml-lint.rake
index ad2d034b0b4..5c0cc4990fc 100644
--- a/lib/tasks/haml-lint.rake
+++ b/lib/tasks/haml-lint.rake
@@ -2,5 +2,14 @@ unless Rails.env.production?
require 'haml_lint/rake_task'
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
end
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index c996537cfbe..31cbd651edb 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -1,16 +1,14 @@
-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'
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
AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
diff --git a/qa/qa.rb b/qa/qa.rb
index 180ee778fd4..bd24f241747 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -27,6 +27,7 @@ module QA
module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox'
autoload :Group, 'qa/factory/resource/group'
+ autoload :Issue, 'qa/factory/resource/issue'
autoload :Project, 'qa/factory/resource/project'
autoload :MergeRequest, 'qa/factory/resource/merge_request'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
@@ -125,6 +126,12 @@ module QA
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
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
module Profile
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
new file mode 100644
index 00000000000..06e7e8df56c
--- /dev/null
+++ b/qa/qa/factory/resource/issue.rb
@@ -0,0 +1,34 @@
+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
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index f472e8ccc7e..7a2d9731205 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -42,6 +42,23 @@ module QA
page.within(selector) { yield } if block_given?
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)
find(element_selector_css(name))
end
@@ -80,6 +97,21 @@ module QA
views.map(&:errors).flatten
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
attr_reader :views
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index ac1fd9b5f90..82985703e54 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -3,23 +3,14 @@ module QA
module Group
class Show < Page::Base
view 'app/views/groups/show.html.haml' do
- element :dropdown_toggle, '.dropdown-toggle'
- element :new_project_subgroup, '.new-project-subgroup'
-
- element :new_project_toggle,
- /%li.+ data: { value: "new\-project"/
- element :new_project_button,
- /%input.+ data: { action: "new\-project"/
-
- 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...')"
+ element :new_project_or_subgroup_dropdown, '.new-project-subgroup'
+ element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle'
+ element :new_project_option, /%li.*data:.*value: "new-project"/
+ element :new_project_button, /%input.*data:.*action: "new-project"/
+ element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/
+
+ # data-value and data-action get modified by JS for subgroup
+ element :new_subgroup_button, /%input.*\.js-new-group-child/
end
def go_to_subgroup(name)
diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb
index b2738152907..5fdcea20029 100644
--- a/qa/qa/page/menu/side.rb
+++ b/qa/qa/page/menu/side.rb
@@ -7,6 +7,8 @@ module QA
element :settings_link, 'link_to edit_project_path'
element :repository_link, "title: 'Repository'"
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 :activity_link, "title: 'Activity'"
end
@@ -43,6 +45,12 @@ module QA
end
end
+ def click_issues
+ within_sidebar do
+ click_link('Issues')
+ end
+ end
+
private
def hover_settings
diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb
new file mode 100644
index 00000000000..b5903f536a4
--- /dev/null
+++ b/qa/qa/page/project/issue/index.rb
@@ -0,0 +1,17 @@
+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
diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb
new file mode 100644
index 00000000000..7fc581da1ed
--- /dev/null
+++ b/qa/qa/page/project/issue/new.rb
@@ -0,0 +1,33 @@
+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
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
new file mode 100644
index 00000000000..10644c0fecc
--- /dev/null
+++ b/qa/qa/page/project/issue/show.rb
@@ -0,0 +1,37 @@
+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
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 75308ae8a3c..553d35f9579 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -17,6 +17,11 @@ module QA
element :project_name
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
wait(reload: false) do
click_element :clone_dropdown
@@ -46,6 +51,12 @@ module QA
sleep 5
refresh
end
+
+ def go_to_new_issue
+ click_element :new_menu_toggle
+
+ click_link 'New issue'
+ end
end
end
end
diff --git a/qa/spec/fixtures/banana_sample.gif b/qa/spec/fixtures/banana_sample.gif
new file mode 100644
index 00000000000..1322ac92d14
--- /dev/null
+++ b/qa/spec/fixtures/banana_sample.gif
Binary files differ
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 96d08287ded..bdb88f3cb57 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -1,6 +1,29 @@
#!/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 = [
%w[bundle exec rake config_lint],
@@ -12,23 +35,20 @@ tasks = [
%w[bundle exec rubocop --parallel],
%w[bundle exec rake gettext:lint],
%w[bundle exec rake lint:static_verification],
- %w[scripts/lint-changelog-yaml],
%w[scripts/lint-conflicts.sh],
%w[scripts/lint-rugged]
]
-failed_tasks = tasks.reduce({}) do |failures, task|
- start = Time.now
- puts
- puts "$ #{task.join(' ')}"
+static_analysis = Gitlab::Popen::Runner.new
- output, status = Gitlab::Popen.popen(task)
- puts "==> Finished in #{Time.now - start} seconds"
+static_analysis.run(tasks) do |cmd, &run|
puts
+ puts "$ #{cmd.join(' ')}"
- failures[task.join(' ')] = output unless status.zero?
+ result = run.call
- failures
+ puts "==> Finished in #{result.duration} seconds"
+ puts
end
puts
@@ -36,17 +56,20 @@ puts '==================================================='
puts
puts
-if failed_tasks.empty?
+if static_analysis.all_success_and_clean?
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
puts 'Some static analyses failed:'
- failed_tasks.each do |failed_task, output|
- puts
- puts "**** #{failed_task} failed with the following error:"
- puts
- puts output
- end
+ emit_warnings(static_analysis)
+ emit_errors(static_analysis)
exit 1
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 1dd7547a7fc..31862b2e8f4 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do
wait_for_requests
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
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 69e4c9f04a1..89d3bd24b89 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do
sign_in(user)
end
- def edit_and_commit
+ def edit_and_commit(commit_changes: true)
wait_for_requests
find('.js-edit-blob').click
find('#editor')
execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
- click_button 'Commit changes'
+
+ if commit_changes
+ click_button 'Commit changes'
+ end
end
context 'from MR diff' do
@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do
context 'from blob file path' do
before do
visit project_blob_path(project, tree_join(branch, file_path))
- edit_and_commit
end
it 'updates content' do
+ edit_and_commit
+
expect(page).to have_content 'successfully committed'
expect(page).to have_content 'NextFeature'
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
diff --git a/spec/initializers/grape_route_helpers_fix_spec.rb b/spec/initializers/grape_route_helpers_fix_spec.rb
new file mode 100644
index 00000000000..2cf5924128f
--- /dev/null
+++ b/spec/initializers/grape_route_helpers_fix_spec.rb
@@ -0,0 +1,14 @@
+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
diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js
index c8b00a4f553..143137c23ec 100644
--- a/spec/javascripts/create_item_dropdown_spec.js
+++ b/spec/javascripts/create_item_dropdown_spec.js
@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => {
preloadFixtures('static/create_item_dropdown.html.raw');
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(() => {
loadFixtures('static/create_item_dropdown.html.raw');
$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(() => {
$wrapperEl.remove();
});
- it('should have a dropdown item for each piece of data', () => {
- // Get the data in the dropdown
- $('.js-dropdown-menu-toggle').click();
+ describe('items', () => {
+ 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 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');
- expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
+ const $itemEls = $wrapperEl.find('.js-dropdown-content a');
+ expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
+ });
});
describe('created items', () => {
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(() => {
+ 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
$('.js-dropdown-menu-toggle').click();
@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => {
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');
+ });
+ });
});
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 9033eb9ce02..d0fba908e34 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -1,3 +1,5 @@
+import MockAdaptor from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
describe('IntegrationSettingsForm', () => {
@@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => {
describe('testSettings', () => {
let integrationSettingsForm;
let formData;
+ let mock;
beforeEach(() => {
+ mock = new MockAdaptor(axios);
+
+ spyOn(axios, 'put').and.callThrough();
+
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
formData = integrationSettingsForm.$form.serialize();
});
- it('should make an ajax request with provided `formData`', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
- 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({
- type: 'PUT',
- url: integrationSettingsForm.testEndPoint,
- data: formData,
- });
+ done();
+ })
+ .catch(done.fail);
});
- 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 deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- integrationSettingsForm.testSettings(formData);
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ 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');
- 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');
+ done();
+ })
+ .catch(done.fail);
});
- it('should submit form if ajax request responds without any error in test', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should submit form if ajax request responds without any error in test', (done) => {
+ spyOn(integrationSettingsForm.$form, 'submit');
- integrationSettingsForm.testSettings(formData);
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ });
- spyOn(integrationSettingsForm.$form, 'submit');
- deferred.resolve({ error: false });
+ integrationSettingsForm.testSettings(formData)
+ .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', () => {
- const errorMessage = 'Test failed.';
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should submit form when clicked on `Save anyway` action of error Flash', (done) => {
+ spyOn(integrationSettingsForm.$form, 'submit');
- 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');
- expect($flashAction).toBeDefined();
+ $flashAction.get(0).click();
+ })
+ .then(() => {
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
- spyOn(integrationSettingsForm.$form, 'submit');
- $flashAction.get(0).click();
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ done();
+ })
+ .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 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', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- integrationSettingsForm.testSettings(formData);
+ it('should always call `toggleSubmitBtnState` with `false` once request is completed', (done) => {
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
- deferred.reject();
- expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+
+ done();
+ })
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
index 5a9112716f4..d53ffecbd35 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/javascripts/issuable_spec.js
@@ -1,3 +1,5 @@
+import MockAdaptor from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
describe('Issuable', () => {
@@ -19,6 +21,8 @@ describe('Issuable', () => {
});
describe('resetIncomingEmailToken', () => {
+ let mock;
+
beforeEach(() => {
const element = document.createElement('a');
element.classList.add('incoming-email-token-reset');
@@ -30,14 +34,28 @@ describe('Issuable', () => {
document.body.appendChild(input);
Issuable = new IssuableIndex('issue_');
+
+ mock = new MockAdaptor(axios);
+
+ mock.onPut('foo').reply(200, {
+ new_address: 'testing123',
+ });
});
- it('should send request to reset email token', () => {
- spyOn(jQuery, 'ajax').and.callThrough();
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should send request to reset email token', (done) => {
+ spyOn(axios, 'put').and.callThrough();
document.querySelector('.incoming-email-token-reset').click();
- expect(jQuery.ajax).toHaveBeenCalled();
- expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo');
+ setTimeout(() => {
+ expect(axios.put).toHaveBeenCalledWith('foo');
+ expect($('#issuable_email').val()).toBe('testing123');
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 2dc3b72ea40..43a989393ba 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -1,108 +1,99 @@
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';
-
-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 },
- });
-};
+import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetMerged', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr, service } = mergedComponent.props;
-
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
-
- expect(service.type instanceof Object).toBeTruthy();
- expect(service.required).toBeTruthy();
- });
- });
-
- describe('components', () => {
- it('should have components added', () => {
- expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined();
- });
+ let vm;
+ const targetBranch = 'foo';
+
+ beforeEach(() => {
+ const Component = Vue.extend(mergedComponent);
+ const mr = {
+ isRemovingSourceBranch: false,
+ cherryPickInForkPath: false,
+ canCherryPickInCurrentMR: true,
+ revertInForkPath: false,
+ canRevertInCurrentMR: true,
+ canRemoveSourceBranch: true,
+ sourceBranchRemoved: true,
+ metrics: {
+ 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', () => {
- it('should have default data', () => {
- const data = mergedComponent.data();
-
- expect(data.isMakingRequest).toBeFalsy();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
describe('shouldShowRemoveSourceBranch', () => {
- it('should correct value when fields changed', () => {
- const vm = createComponent();
+ it('returns true when sourceBranchRemoved is 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;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+ it('returns false when canRemoveSourceBranch is false', () => {
vm.mr.sourceBranchRemoved = 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.isMakingRequest = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+ it('returns true when all are true', () => {
vm.mr.isRemovingSourceBranch = true;
vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
});
});
+
describe('shouldShowSourceBranchRemoving', () => {
it('should correct value when fields changed', () => {
- const vm = createComponent();
vm.mr.sourceBranchRemoved = false;
- expect(vm.shouldShowSourceBranchRemoving).toBeFalsy();
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(false);
vm.mr.sourceBranchRemoved = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
vm.mr.sourceBranchRemoved = false;
vm.isMakingRequest = true;
- expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
vm.isMakingRequest = false;
vm.mr.isRemovingSourceBranch = true;
- expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
});
});
});
@@ -110,8 +101,6 @@ describe('MRWidgetMerged', () => {
describe('methods', () => {
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
- const vm = createComponent();
- spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => {
resolve({
data: {
@@ -123,7 +112,7 @@ describe('MRWidgetMerged', () => {
vm.removeSourceBranch();
setTimeout(() => {
const args = eventHub.$emit.calls.argsFor(0);
- expect(vm.isMakingRequest).toBeTruthy();
+ expect(vm.isMakingRequest).toEqual(true);
expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).not.toThrow();
done();
@@ -132,53 +121,50 @@ describe('MRWidgetMerged', () => {
});
});
- describe('template', () => {
- let vm;
- let el;
+ it('has merged by information', () => {
+ expect(vm.$el.textContent).toContain('Merged by');
+ expect(vm.$el.textContent).toContain('Administrator');
+ });
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
- });
+ it('renders branch information', () => {
+ expect(vm.$el.textContent).toContain('The changes were merged into');
+ expect(vm.$el.textContent).toContain(targetBranch);
+ });
- it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.querySelector('.js-mr-widget-author')).toBeDefined();
- expect(el.innerText).toContain('The changes were merged into');
- expect(el.innerText).toContain(targetBranch);
- expect(el.innerText).toContain('The source branch has been removed');
- expect(el.innerText).toContain('Revert');
- expect(el.innerText).toContain('Cherry-pick');
- expect(el.innerText).not.toContain('You can remove source branch now');
- expect(el.innerText).not.toContain('The source branch is being removed');
- });
+ it('renders information about branch being removed', () => {
+ expect(vm.$el.textContent).toContain('The source branch has been removed');
+ });
- it('should not show source branch removed text', (done) => {
- vm.mr.sourceBranchRemoved = false;
+ it('shows revert and cherry-pick buttons', () => {
+ expect(vm.$el.textContent).toContain('Revert');
+ expect(vm.$el.textContent).toContain('Cherry-pick');
+ });
- Vue.nextTick(() => {
- expect(el.innerText).toContain('You can remove source branch now');
- expect(el.innerText).not.toContain('The source branch has been removed');
- done();
- });
+ it('should not show source branch removed text', (done) => {
+ vm.mr.sourceBranchRemoved = false;
+
+ 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) => {
- vm.mr.isRemovingSourceBranch = true;
- vm.mr.sourceBranchRemoved = false;
+ it('should show source branch removing text', (done) => {
+ vm.mr.isRemovingSourceBranch = true;
+ vm.mr.sourceBranchRemoved = false;
- Vue.nextTick(() => {
- expect(el.innerText).toContain('The source branch is being removed');
- expect(el.innerText).not.toContain('You can remove source branch now');
- expect(el.innerText).not.toContain('The source branch has been removed');
- done();
- });
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('The source branch is being removed');
+ expect(vm.$el.innerText).not.toContain('You can remove source branch now');
+ expect(vm.$el.innerText).not.toContain('The source branch has been removed');
+ done();
});
+ });
- it('should use mergedEvent updatedAt as tooltip title', () => {
- expect(
- el.querySelector('time').getAttribute('title'),
- ).toBe('mergedUpdatedAt');
- });
+ it('should use mergedEvent mergedAt as tooltip title', () => {
+ expect(
+ vm.$el.querySelector('time').getAttribute('title'),
+ ).toBe('Jan 24, 2018 1:02pm GMT+0000');
});
});
diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb
new file mode 100644
index 00000000000..2e2cb4ca28f
--- /dev/null
+++ b/spec/lib/gitlab/popen/runner_spec.rb
@@ -0,0 +1,139 @@
+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
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index b145ca36f26..1dbead16d5b 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -1,11 +1,23 @@
require 'spec_helper'
-describe 'Gitlab::Popen' do
+describe Gitlab::Popen do
let(:path) { Rails.root.join('tmp').to_s }
before do
@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
context 'zero status' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 45a606c1ea8..f5b3b4a9fc5 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -277,7 +277,7 @@ describe Ci::Build do
allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1)
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
context 'when project does not have jobs_cache_index' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index f8a98b43e46..959383ff0b7 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -228,7 +228,7 @@ eos
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[: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[:removed]).to eq([]) }
end
@@ -532,8 +532,8 @@ eos
let(:commit2) { merge_request1.merge_request_diff.commits.first }
it 'returns merge_requests that introduced that commit' do
- expect(commit1.merge_requests).to eq([merge_request1, merge_request2])
- expect(commit2.merge_requests).to eq([merge_request1])
+ expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2)
+ expect(commit2.merge_requests).to contain_exactly(merge_request1)
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 15f9c68e400..eb9690df313 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1539,7 +1539,7 @@ describe MergeRequest do
expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
end
- it "executs diff cache service" do
+ it "executes diff cache service" do
expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
subject.reload_diff
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index c9b3c6cf602..1eaaadf56c5 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -3,6 +3,29 @@ require 'spec_helper'
describe JiraService do
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
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 8e2982f1a5d..14dd9da119d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -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, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
+
expect do
get api("/projects/#{project.id}/merge_requests", user)
end.not_to exceed_query_limit(control)
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7a01d3dd698..7c3374c6113 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -55,11 +55,12 @@ describe MergeRequests::RefreshService do
before do
allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- reload_mrs
end
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)
.with(@merge_request, 'update', old_rev: @oldrev)
@@ -72,6 +73,26 @@ describe MergeRequests::RefreshService do
expect(@build_failed_todo).to be_done
expect(@fork_build_failed_todo).to be_done
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
context 'when pipeline exists for the source branch' do
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index 923c8080e6c..2197bc9d853 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -1,6 +1,5 @@
require 'action_dispatch/testing/test_request'
require 'fileutils'
-require 'gitlab/popen'
module JavaScriptFixturesHelpers
include Gitlab::Popen
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index fae5ec35c47..e9322ec4931 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require 'tasks/gitlab/task_helpers'
class TestHelpersTest
include Gitlab::TaskHelpers