Commit 3f75593b authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into ee-38175-add-domain-field-to-auto-devops-application-setting

parents 0a6816eb 2516ae71
...@@ -13,3 +13,5 @@ lib/gitlab/redis/*.rb ...@@ -13,3 +13,5 @@ lib/gitlab/redis/*.rb
lib/gitlab/gitaly_client/operation_service.rb lib/gitlab/gitaly_client/operation_service.rb
app/models/project_services/packagist_service.rb app/models/project_services/packagist_service.rb
lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
lib/gitlab/background_migration/*
app/models/project_services/kubernetes_service.rb
...@@ -361,7 +361,7 @@ group :development, :test do ...@@ -361,7 +361,7 @@ group :development, :test do
gem 'scss_lint', '~> 0.56.0', require: false gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.0', require: false gem 'haml_lint', '~> 0.26.0', require: false
gem 'simplecov', '~> 0.14.0', require: false gem 'simplecov', '~> 0.14.0', require: false
gem 'flay', '~> 2.8.0', require: false gem 'flay', '~> 2.10.0', require: false
gem 'bundler-audit', '~> 0.5.0', require: false gem 'bundler-audit', '~> 0.5.0', require: false
gem 'benchmark-ips', '~> 2.3.0', require: false gem 'benchmark-ips', '~> 2.3.0', require: false
......
...@@ -235,7 +235,7 @@ GEM ...@@ -235,7 +235,7 @@ GEM
fast_gettext (1.4.0) fast_gettext (1.4.0)
ffaker (2.4.0) ffaker (2.4.0)
ffi (1.9.18) ffi (1.9.18)
flay (2.8.1) flay (2.10.0)
erubis (~> 2.7.0) erubis (~> 2.7.0)
path_expander (~> 1.0) path_expander (~> 1.0)
ruby_parser (~> 3.0) ruby_parser (~> 3.0)
...@@ -617,7 +617,7 @@ GEM ...@@ -617,7 +617,7 @@ GEM
ast (~> 2.3) ast (~> 2.3)
parslet (1.5.0) parslet (1.5.0)
blankslate (~> 2.0) blankslate (~> 2.0)
path_expander (1.0.1) path_expander (1.0.2)
peek (1.0.1) peek (1.0.1)
concurrent-ruby (>= 0.9.0) concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0) concurrent-ruby-ext (>= 0.9.0)
...@@ -1072,7 +1072,7 @@ DEPENDENCIES ...@@ -1072,7 +1072,7 @@ DEPENDENCIES
faraday_middleware-aws-signers-v4 faraday_middleware-aws-signers-v4
fast_blank fast_blank
ffaker (~> 2.4) ffaker (~> 2.4)
flay (~> 2.8.0) flay (~> 2.10.0)
flipper (~> 0.11.0) flipper (~> 0.11.0)
flipper-active_record (~> 0.11.0) flipper-active_record (~> 0.11.0)
flipper-active_support_cache_store (~> 0.11.0) flipper-active_support_cache_store (~> 0.11.0)
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, max-len */
import 'vendor/jquery.waitforimages'; import 'vendor/jquery.waitforimages';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility'; import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash'; import flash from './flash';
import TaskList from './task_list'; import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown'; import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper'; import IssuablesHelper from './helpers/issuables_helper';
...@@ -42,12 +43,8 @@ export default class Issue { ...@@ -42,12 +43,8 @@ export default class Issue {
this.disableCloseReopenButton($button); this.disableCloseReopenButton($button);
url = $button.attr('href'); url = $button.attr('href');
return $.ajax({ return axios.put(url)
type: 'PUT', .then(({ data }) => {
url: url
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
const isClosedBadge = $('div.status-box-issue-closed'); const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open'); const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter'); const projectIssuesCounter = $('.issue_counter');
...@@ -74,9 +71,10 @@ export default class Issue { ...@@ -74,9 +71,10 @@ export default class Issue {
} }
} }
} else { } else {
new Flash(issueFailMessage); flash(issueFailMessage);
} }
}) })
.catch(() => flash(issueFailMessage))
.then(() => { .then(() => {
this.disableCloseReopenButton($button, false); this.disableCloseReopenButton($button, false);
}); });
...@@ -115,24 +113,22 @@ export default class Issue { ...@@ -115,24 +113,22 @@ export default class Issue {
static initMergeRequests() { static initMergeRequests() {
var $container; var $container;
$container = $('#merge-requests'); $container = $('#merge-requests');
return $.getJSON($container.data('url')).fail(function() { return axios.get($container.data('url'))
return new Flash('Failed to load referenced merge requests'); .then(({ data }) => {
}).done(function(data) { if ('html' in data) {
if ('html' in data) { $container.html(data.html);
return $container.html(data.html); }
} }).catch(() => flash('Failed to load referenced merge requests'));
});
} }
static initRelatedBranches() { static initRelatedBranches() {
var $container; var $container;
$container = $('#related-branches'); $container = $('#related-branches');
return $.getJSON($container.data('url')).fail(function() { return axios.get($container.data('url'))
return new Flash('Failed to load related branches'); .then(({ data }) => {
}).done(function(data) { if ('html' in data) {
if ('html' in data) { $container.html(data.html);
return $container.html(data.html); }
} }).catch(() => flash('Failed to load related branches'));
});
} }
} }
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility'; import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints'; import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils'; import { numberToHumanSize } from './lib/utils/number_utils';
...@@ -8,6 +9,7 @@ export default class Job { ...@@ -8,6 +9,7 @@ export default class Job {
constructor(options) { constructor(options) {
this.timeout = null; this.timeout = null;
this.state = null; this.state = null;
this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath; this.pagePath = this.options.pagePath;
...@@ -171,12 +173,23 @@ export default class Job { ...@@ -171,12 +173,23 @@ export default class Job {
} }
getBuildTrace() { getBuildTrace() {
return $.ajax({ return axios.get(`${this.pagePath}/trace.json`, {
url: `${this.pagePath}/trace.json`, params: { state: this.state },
data: { state: this.state },
}) })
.done((log) => { .then((res) => {
setCiStatusFavicon(`${this.pagePath}/status.json`); const log = res.data;
if (!this.fetchingStatusFavicon) {
this.fetchingStatusFavicon = true;
setCiStatusFavicon(`${this.pagePath}/status.json`)
.then(() => {
this.fetchingStatusFavicon = false;
})
.catch(() => {
this.fetchingStatusFavicon = false;
});
}
if (log.state) { if (log.state) {
this.state = log.state; this.state = log.state;
...@@ -217,7 +230,7 @@ export default class Job { ...@@ -217,7 +230,7 @@ export default class Job {
visitUrl(this.pagePath); visitUrl(this.pagePath);
} }
}) })
.fail(() => { .catch(() => {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
}) })
.then(() => { .then(() => {
......
...@@ -2,9 +2,12 @@ ...@@ -2,9 +2,12 @@
/* global Issuable */ /* global Issuable */
/* global ListLabel */ /* global ListLabel */
import _ from 'underscore'; import _ from 'underscore';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils'; import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label'; import CreateLabelDropdown from './create_label';
import flash from './flash';
export default class LabelsSelect { export default class LabelsSelect {
constructor(els, options = {}) { constructor(els, options = {}) {
...@@ -82,99 +85,96 @@ export default class LabelsSelect { ...@@ -82,99 +85,96 @@ export default class LabelsSelect {
} }
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ axios.put(issueUpdateURL, data)
type: 'PUT', .then(({ data }) => {
url: issueUpdateURL, var labelCount, template, labelTooltipTitle, labelTitles;
dataType: 'JSON', $loading.fadeOut();
data: data $dropdown.trigger('loaded.gl.dropdown');
}).done(function(data) { $selectbox.hide();
var labelCount, template, labelTooltipTitle, labelTitles; data.issueURLSplit = issueURLSplit;
$loading.fadeOut(); labelCount = 0;
$dropdown.trigger('loaded.gl.dropdown'); if (data.labels.length) {
$selectbox.hide(); template = labelHTMLTemplate(data);
data.issueURLSplit = issueURLSplit; labelCount = data.labels.length;
labelCount = 0; }
if (data.labels.length) { else {
template = labelHTMLTemplate(data); template = labelNoneHTMLTemplate;
labelCount = data.labels.length; }
} $value.removeAttr('style').html(template);
else { $sidebarCollapsedValue.text(labelCount);
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) { if (data.labels.length) {
labelTitles = data.labels.map(function(label) { labelTitles = data.labels.map(function(label) {
return label.title; return label.title;
}); });
if (labelTitles.length > 5) { if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5); labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more'); labelTitles.push('and ' + (data.labels.length - 5) + ' more');
} }
labelTooltipTitle = labelTitles.join(', '); labelTooltipTitle = labelTitles.join(', ');
} }
else { else {
labelTooltipTitle = ''; labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy'); $sidebarLabelTooltip.tooltip('destroy');
} }
$sidebarLabelTooltip $sidebarLabelTooltip
.attr('title', labelTooltipTitle) .attr('title', labelTooltipTitle)
.tooltip('fixTitle'); .tooltip('fixTitle');
$('.has-tooltip', $value).tooltip({ $('.has-tooltip', $value).tooltip({
container: 'body' container: 'body'
}); });
}); })
.catch(() => flash(__('Error saving label update.')));
}; };
$dropdown.glDropdown({ $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
return $.ajax({ axios.get(labelUrl)
url: labelUrl .then((res) => {
}).done(function(data) { let data = _.chain(res.data).groupBy(function(label) {
data = _.chain(data).groupBy(function(label) { return label.title;
return label.title; }).map(function(label) {
}).map(function(label) { var color;
var color; color = _.map(label, function(dup) {
color = _.map(label, function(dup) { return dup.color;
return dup.color;
});
return {
id: label[0].id,
title: label[0].title,
color: color,
duplicate: color.length > 1
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
title: 'No Label'
}); });
return {
id: label[0].id,
title: label[0].title,
color: color,
duplicate: color.length > 1
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
var extraData = [];
if (showNo) {
extraData.unshift({
id: 0,
title: 'No Label'
});
}
if (showAny) {
extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
if (extraData.length) {
extraData.push('divider');
data = extraData.concat(data);
}
} }
if (showAny) {
extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
if (extraData.length) {
extraData.push('divider');
data = extraData.concat(data);
}
}
callback(data); callback(data);
if (showMenuAbove) { if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove(); $dropdown.data('glDropdown').positionMenuAbove();
} }
}); })
.catch(() => flash(__('Error fetching labels.')));
}, },
renderRow: function(label, instance) { renderRow: function(label, instance) {
var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue; var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
......
import axios from './axios_utils';
import Cache from './cache'; import Cache from './cache';
class AjaxCache extends Cache { class AjaxCache extends Cache {
...@@ -18,25 +19,18 @@ class AjaxCache extends Cache { ...@@ -18,25 +19,18 @@ class AjaxCache extends Cache {
let pendingRequest = this.pendingRequests[endpoint]; let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) { if (!pendingRequest) {
pendingRequest = new Promise((resolve, reject) => { pendingRequest = axios.get(endpoint)
// jQuery 2 is not Promises/A+ compatible (missing catch) .then(({ data }) => {
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return this.internalStorage[endpoint] = data;
.then(data => resolve(data), delete this.pendingRequests[endpoint];
(jqXHR, textStatus, errorThrown) => { })
const error = new Error(`${endpoint}: ${errorThrown}`); .catch((e) => {
error.textStatus = textStatus; const error = new Error(`${endpoint}: ${e.message}`);
reject(error); error.textStatus = e.message;
},
); delete this.pendingRequests[endpoint];
}) throw error;
.then((data) => { });
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((error) => {
delete this.pendingRequests[endpoint];
throw error;
});
this.pendingRequests[endpoint] = pendingRequest; this.pendingRequests[endpoint] = pendingRequest;
} }
......
...@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => { ...@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => {
window.activeVueResources -= 1; window.activeVueResources -= 1;
return config; return config;
}, (e) => {
window.activeVueResources -= 1;
return Promise.reject(e);
}); });
export default axios; export default axios;
......
import { getLocationHash } from './url_utility';
import axios from './axios_utils'; import axios from './axios_utils';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index]; export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
...@@ -28,16 +28,11 @@ export const isInIssuePage = () => { ...@@ -28,16 +28,11 @@ export const isInIssuePage = () => {
return page === 'issues' && action === 'show'; return page === 'issues' && action === 'show';
}; };
export const ajaxGet = url => $.ajax({ export const ajaxGet = url => axios.get(url, {
type: 'GET', params: { format: 'js' },
url, responseType: 'text',
dataType: 'script', }).then(({ data }) => {
}); $.globalEval(data);
export const ajaxPost = (url, data) => $.ajax({
type: 'POST',
url,
data,
}); });
export const rstrip = (val) => { export const rstrip = (val) => {
...@@ -412,7 +407,6 @@ window.gl.utils = { ...@@ -412,7 +407,6 @@ window.gl.utils = {
getGroupSlug, getGroupSlug,
isInIssuePage, isInIssuePage,
ajaxGet, ajaxGet,
ajaxPost,
rstrip, rstrip,
updateTooltipTitle, updateTooltipTitle,
disableButtonIfEmptyField, disableButtonIfEmptyField,
......
/* eslint-disable no-param-reassign, comma-dangle */ /* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
((global) => { ((global) => {
global.mergeConflicts = global.mergeConflicts || {}; global.mergeConflicts = global.mergeConflicts || {};
...@@ -10,20 +11,11 @@ ...@@ -10,20 +11,11 @@
} }
fetchConflictsData() { fetchConflictsData() {
return $.ajax({ return axios.get(this.conflictsPath);
dataType: 'json',
url: this.conflictsPath
});
} }
submitResolveConflicts(data) { submitResolveConflicts(data) {
return $.ajax({ return axios.post(this.resolveConflictsPath, data);
url: this.resolveConflictsPath,
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
method: 'POST'
});
} }
} }
......
...@@ -38,24 +38,23 @@ $(() => { ...@@ -38,24 +38,23 @@ $(() => {
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); } showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
}, },
created() { created() {
mergeConflictsService mergeConflictsService.fetchConflictsData()
.fetchConflictsData() .then(({ data }) => {
.done((data) => {
if (data.type === 'error') { if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message); mergeConflictsStore.setFailedRequest(data.message);
} else { } else {
mergeConflictsStore.setConflictsData(data); mergeConflictsStore.setConflictsData(data);
} }
})
.error(() => {
mergeConflictsStore.setFailedRequest();
})
.always(() => {
mergeConflictsStore.setLoadingState(false); mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => { this.$nextTick(() => {
syntaxHighlight($('.js-syntax-highlight')); syntaxHighlight($('.js-syntax-highlight'));
}); });
})
.catch(() => {
mergeConflictsStore.setLoadingState(false);
mergeConflictsStore.setFailedRequest();
}); });
}, },
methods: { methods: {
...@@ -82,10 +81,10 @@ $(() => { ...@@ -82,10 +81,10 @@ $(() => {
mergeConflictsService mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData()) .submitResolveConflicts(mergeConflictsStore.getCommitData())
.done((data) => { .then(({ data }) => {
window.location.href = data.redirect_to; window.location.href = data.redirect_to;
}) })
.error(() => { .catch(() => {
mergeConflictsStore.setSubmitState(false); mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!'); new Flash('Failed to save merge conflicts resolutions. Please try again!');
}); });
......
/* eslint-disable no-new, class-methods-use-this */ /* eslint-disable no-new, class-methods-use-this */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Flash from './flash'; import axios from './lib/utils/axios_utils';
import flash from './flash';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import initChangesDropdown from './init_changes_dropdown'; import initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints'; import bp from './breakpoints';
...@@ -244,15 +245,22 @@ export default class MergeRequestTabs { ...@@ -244,15 +245,22 @@ export default class MergeRequestTabs {
if (this.commitsLoaded) { if (this.commitsLoaded) {
return; return;
} }
this.ajaxGet({
url: `${source}.json`, this.toggleLoading(true);
success: (data) => {
axios.get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html; document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits')); localTimeAgo($('.js-timeago', 'div#commits'));
this.commitsLoaded = true; this.commitsLoaded = true;
this.scrollToElement('#commits'); this.scrollToElement('#commits');
},
}); this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
} }
mountPipelinesView() { mountPipelinesView() {
...@@ -283,9 +291,10 @@ export default class MergeRequestTabs { ...@@ -283,9 +291,10 @@ export default class MergeRequestTabs {
// some pages like MergeRequestsController#new has query parameters on that anchor // some pages like MergeRequestsController#new has query parameters on that anchor
const urlPathname = parseUrlPathname(source); const urlPathname = parseUrlPathname(source);
this.ajaxGet({ this.toggleLoading(true);
url: `${urlPathname}.json${location.search}`,
success: (data) => { axios.get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs'); const $container = $('#diffs');
$container.html(data.html); $container.html(data.html);
...@@ -335,8 +344,13 @@ export default class MergeRequestTabs { ...@@ -335,8 +344,13 @@ export default class MergeRequestTabs {
// (discussion and diff tabs) and `:target` only applies to the first // (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target'); anchor.addClass('target');
} }
},
}); this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
} }
// Show or hide the loading spinner // Show or hide the loading spinner
...@@ -346,17 +360,6 @@ export default class MergeRequestTabs { ...@@ -346,17 +360,6 @@ export default class MergeRequestTabs {
$('.mr-loading-status .loading').toggle(status); $('.mr-loading-status .loading').toggle(status);
} }
ajaxGet(options) {
const defaults = {
beforeSend: () => this.toggleLoading(true),
error: () => new Flash('An error occurred while fetching this tab.', 'alert'),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
};
$.ajax($.extend({}, defaults, options));
}
diffViewType() { diffViewType() {
return $('.inline-parallel-buttons a.active').data('view-type'); return $('.inline-parallel-buttons a.active').data('view-type');
} }
......
import Flash from './flash'; import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class Milestone { export default class Milestone {
constructor() { constructor() {
...@@ -33,15 +34,12 @@ export default class Milestone { ...@@ -33,15 +34,12 @@ export default class Milestone {
const tabElId = $target.attr('href'); const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) { if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({ axios.get(endpoint)
url: endpoint, .then(({ data }) => {
dataType: 'JSON', $(tabElId).html(data.html);
}) $target.addClass('is-loaded');
.fail(() => new Flash('Error loading milestone tab')) })
.done((data) => { .catch(() => flash('Error loading milestone tab'));
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
} }
} }
} }
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global Issuable */ /* global Issuable */
/* global ListMilestone */ /* global ListMilestone */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility'; import { timeFor } from './lib/utils/datetime_utility';
export default class MilestoneSelect { export default class MilestoneSelect {
...@@ -52,48 +53,47 @@ export default class MilestoneSelect { ...@@ -52,48 +53,47 @@ export default class MilestoneSelect {
} }
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: (term, callback) => $.ajax({ data: (term, callback) => axios.get(milestonesUrl)
url: milestonesUrl .then(({ data }) => {
}).done((data) => { const extraOptions = [];
const extraOptions = []; if (showAny) {
if (showAny) { extraOptions.push({
extraOptions.push({ id: 0,
id: 0, name: '',
name: '', title: 'Any Milestone'
title: 'Any Milestone' });
}); }
} if (showNo) {
if (showNo) { extraOptions.push({
extraOptions.push({ id: -1,
id: -1, name: 'No Milestone',
name: 'No Milestone', title: 'No Milestone'
title: 'No Milestone' });
}); }
} if (showUpcoming) {
if (showUpcoming) { extraOptions.push({
extraOptions.push({ id: -2,
id: -2, name: '#upcoming',
name: '#upcoming', title: 'Upcoming'
title: 'Upcoming' });
}); }
} if (showStarted) {
if (showStarted) { extraOptions.push({
extraOptions.push({ id: -3,
id: -3, name: '#started',
name: '#started', title: 'Started'
title: 'Started' });
}); }
} if (extraOptions.length) {
if (extraOptions.length) { extraOptions.push('divider');
extraOptions.push('divider'); }
}
callback(extraOptions.concat(data)); callback(extraOptions.concat(data));
if (showMenuAbove) { if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove(); $dropdown.data('glDropdown').positionMenuAbove();
} }
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active'); $(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}), }),
renderRow: milestone => ` renderRow: milestone => `
<li data-milestone-id="${milestone.name}"> <li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'> <a href='#' class='dropdown-menu-milestone-link'>
...@@ -200,26 +200,23 @@ export default class MilestoneSelect { ...@@ -200,26 +200,23 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null; data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return axios.put(issueUpdateURL, data)
type: 'PUT', .then(({ data }) => {
url: issueUpdateURL, $dropdown.trigger('loaded.gl.dropdown');
data: data $loading.fadeOut();
}).done((data) => { $selectBox.hide();
$dropdown.trigger('loaded.gl.dropdown'); $value.css('display', '');
$loading.fadeOut(); if (data.milestone != null) {
$selectBox.hide(); data.milestone.full_path = this.currentProject.full_path;
$value.css('display', ''); data.milestone.remaining = timeFor(data.milestone.due_date);
if (data.milestone != null) { data.milestone.name = data.milestone.title;
data.milestone.full_path = this.currentProject.full_path; $value.html(milestoneLinkTemplate(data.milestone));
data.milestone.remaining = timeFor(data.milestone.due_date); return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
data.milestone.name = data.milestone.title; } else {
$value.html(milestoneLinkTemplate(data.milestone)); $value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone)); return $sidebarCollapsedValue.find('span').text('No');
} else { }
$value.html(milestoneLinkNoneTemplate); });
return $sidebarCollapsedValue.find('span').text('No');
}
});
} }
} }
}); });
......
...@@ -24,7 +24,7 @@ import GLForm from './gl_form'; ...@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler'; import loadAwardsHandler from './awards_handler';
import Autosave from './autosave'; import Autosave from './autosave';
import TaskList from './task_list'; import TaskList from './task_list';
import { ajaxPost, isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils'; import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index'; import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
...@@ -1399,7 +1399,7 @@ export default class Notes { ...@@ -1399,7 +1399,7 @@ export default class Notes {
* 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve * 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
* 3) Build temporary placeholder element (using `createPlaceholderNote`) * 3) Build temporary placeholder element (using `createPlaceholderNote`)
* 4) Show placeholder note on UI * 4) Show placeholder note on UI
* 5) Perform network request to submit the note using `ajaxPost` * 5) Perform network request to submit the note using `axios.post`
* a) If request is successfully completed * a) If request is successfully completed
* 1. Remove placeholder element * 1. Remove placeholder element
* 2. Show submitted Note element * 2. Show submitted Note element
...@@ -1481,8 +1481,10 @@ export default class Notes { ...@@ -1481,8 +1481,10 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
// Make request to submit comment on server // Make request to submit comment on server
ajaxPost(formAction, formData) axios.post(formAction, formData)
.then((note) => { .then((res) => {
const note = res.data;
// Submission successful! remove placeholder // Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
...@@ -1555,7 +1557,7 @@ export default class Notes { ...@@ -1555,7 +1557,7 @@ export default class Notes {
} }
$form.trigger('ajax:success', [note]); $form.trigger('ajax:success', [note]);
}).fail(() => { }).catch(() => {
// Submission failed, remove placeholder note and show Flash error message // Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove(); $notesContainer.find(`#${noteUniqueId}`).remove();
...@@ -1594,7 +1596,7 @@ export default class Notes { ...@@ -1594,7 +1596,7 @@ export default class Notes {
* *
* 1) Get Form metadata * 1) Get Form metadata
* 2) Update note element with new content * 2) Update note element with new content
* 3) Perform network request to submit the updated note using `ajaxPost` * 3) Perform network request to submit the updated note using `axios.post`
* a) If request is successfully completed * a) If request is successfully completed
* 1. Show submitted Note element * 1. Show submitted Note element
* b) If request failed * b) If request failed
...@@ -1625,12 +1627,12 @@ export default class Notes { ...@@ -1625,12 +1627,12 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */ /* eslint-disable promise/catch-or-return */
// Make request to update comment on server // Make request to update comment on server
ajaxPost(formAction, formData) axios.post(formAction, formData)
.then((note) => { .then(({ data }) => {
// Submission successful! render final note element // Submission successful! render final note element
this.updateNote(note, $editingNote); this.updateNote(data, $editingNote);
}) })
.fail(() => { .catch(() => {
// Submission failed, revert back to original note // Submission failed, revert back to original note
$noteBodyText.html(_.escape(cachedNoteBodyText)); $noteBodyText.html(_.escape(cachedNoteBodyText));
$editingNote.removeClass('being-posted fade-in'); $editingNote.removeClass('being-posted fade-in');
......
/* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { visitUrl } from '../../lib/utils/url_utility'; import { __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import projectSelect from '../../project_select'; import projectSelect from '../../project_select';
export default class Project { export default class Project {
...@@ -79,17 +82,15 @@ export default class Project { ...@@ -79,17 +82,15 @@ export default class Project {
$dropdown = $(this); $dropdown = $(this);
selected = $dropdown.data('selected'); selected = $dropdown.data('selected');
return $dropdown.glDropdown({ return $dropdown.glDropdown({
data: function(term, callback) { data(term, callback) {
return $.ajax({ axios.get($dropdown.data('refs-url'), {
url: $dropdown.data('refs-url'), params: {
data: {
ref: $dropdown.data('ref'), ref: $dropdown.data('ref'),
search: term, search: term,
}, },
dataType: 'json', })
}).done(function(refs) { .then(({ data }) => callback(data))
return callback(refs); .catch(() => flash(__('An error occurred while getting projects')));
});
}, },
selectable: true, selectable: true,
filterable: true, filterable: true,
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
}; };
</script> </script>
<template> <template>
<ul class="nav-links scrolling-tabs"> <ul class="nav-links scrolling-tabs separator">
<li <li
v-for="(tab, i) in tabs" v-for="(tab, i) in tabs"
:key="i" :key="i"
......
...@@ -82,6 +82,10 @@ ...@@ -82,6 +82,10 @@
/* Small devices (phones, tablets, 768px and lower) */ /* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
width: 100%; width: 100%;
&.mobile-separator {
border-bottom: 1px solid $border-color;
}
} }
} }
...@@ -168,9 +172,9 @@ ...@@ -168,9 +172,9 @@
display: inline-block; display: inline-block;
} }
// Applies on /dashboard/issues
.project-item-select-holder { .project-item-select-holder {
margin: 0; margin: 0;
width: 100%;
} }
&.inline { &.inline {
...@@ -367,7 +371,6 @@ ...@@ -367,7 +371,6 @@
.project-item-select-holder.btn-group { .project-item-select-holder.btn-group {
display: flex; display: flex;
max-width: 350px;
overflow: hidden; overflow: hidden;
float: right; float: right;
......
class Admin::ServicesController < Admin::ApplicationController class Admin::ServicesController < Admin::ApplicationController
include ServiceParams include ServiceParams
before_action :whitelist_query_limiting, only: [:index]
before_action :service, only: [:edit, :update] before_action :service, only: [:edit, :update]
def index def index
...@@ -37,4 +38,8 @@ class Admin::ServicesController < Admin::ApplicationController ...@@ -37,4 +38,8 @@ class Admin::ServicesController < Admin::ApplicationController
def service def service
@service ||= Service.where(id: params[:id], template: true).first @service ||= Service.where(id: params[:id], template: true).first
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42430')
end
end end
...@@ -4,6 +4,7 @@ module Boards ...@@ -4,6 +4,7 @@ module Boards
prepend EE::Boards::IssuesController prepend EE::Boards::IssuesController
include BoardsResponses include BoardsResponses
before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index] before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create] before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update] before_action :authorize_update_issue, only: [:update]
...@@ -94,5 +95,10 @@ module Boards ...@@ -94,5 +95,10 @@ module Boards
} }
) )
end end
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42439
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42428')
end
end end
end end
class Import::GitlabProjectsController < Import::BaseController class Import::GitlabProjectsController < Import::BaseController
before_action :whitelist_query_limiting, only: [:create]
before_action :verify_gitlab_project_import_enabled before_action :verify_gitlab_project_import_enabled
def new def new
...@@ -40,4 +41,8 @@ class Import::GitlabProjectsController < Import::BaseController ...@@ -40,4 +41,8 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file :path, :namespace_id, :file
) )
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437')
end
end end
...@@ -4,6 +4,7 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include RendersCommits include RendersCommits
before_action :whitelist_query_limiting
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars before_action :assign_ref_vars
before_action :authorize_download_code! before_action :authorize_download_code!
...@@ -65,4 +66,8 @@ class Projects::CommitsController < Projects::ApplicationController ...@@ -65,4 +66,8 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = @commits.with_pipeline_status @commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits) @commits = prepare_commits_for_rendering(@commits)
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
end end
...@@ -3,6 +3,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -3,6 +3,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::TextHelper include ActionView::Helpers::TextHelper
include CycleAnalyticsParams include CycleAnalyticsParams
before_action :whitelist_query_limiting, only: [:show]
before_action :authorize_read_cycle_analytics! before_action :authorize_read_cycle_analytics!
def show def show
...@@ -31,4 +32,8 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -31,4 +32,8 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
permissions: @cycle_analytics.permissions(user: current_user) permissions: @cycle_analytics.permissions(user: current_user)
} }
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42671')
end
end end
...@@ -2,6 +2,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::ForksController < Projects::ApplicationController
include ContinueParams include ContinueParams
# Authorize # Authorize
before_action :whitelist_query_limiting, only: [:create]
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create] before_action :authenticate_user!, only: [:new, :create]
...@@ -54,4 +55,8 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -54,4 +55,8 @@ class Projects::ForksController < Projects::ApplicationController
render :error render :error
end end
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42335')
end
end end
...@@ -8,6 +8,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -8,6 +8,8 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new, :export_csv] prepend_before_action :authenticate_user!, only: [:new, :export_csv]
before_action :whitelist_query_limiting_ee, only: [:update]
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available! before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv] before_action :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
...@@ -250,4 +252,17 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -250,4 +252,17 @@ class Projects::IssuesController < Projects::ApplicationController
@finder_type = IssuesFinder @finder_type = IssuesFinder
super super
end end
def whitelist_query_limiting
# Also see the following issues:
#
# 1. https://gitlab.com/gitlab-org/gitlab-ce/issues/42423
# 2. https://gitlab.com/gitlab-org/gitlab-ce/issues/42424
# 3. https://gitlab.com/gitlab-org/gitlab-ce/issues/42426
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42422')
end
def whitelist_query_limiting_ee
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4794')
end
end end
...@@ -6,6 +6,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -6,6 +6,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
prepend ::EE::Projects::MergeRequests::CreationsController prepend ::EE::Projects::MergeRequests::CreationsController
skip_before_action :merge_request skip_before_action :merge_request
before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_create_merge_request! before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path] before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create] before_action :build_merge_request, except: [:create]
...@@ -127,4 +128,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap ...@@ -127,4 +128,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project.forked_from_project @project.forked_from_project
end end
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42384')
end
end end
...@@ -9,6 +9,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -9,6 +9,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
prepend ::EE::Projects::MergeRequestsController prepend ::EE::Projects::MergeRequestsController
skip_before_action :merge_request, only: [:index, :bulk_update] skip_before_action :merge_request, only: [:index, :bulk_update]
before_action :whitelist_query_limiting_ee, only: [:merge, :show]
before_action :whitelist_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort] before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues] before_action :authenticate_user!, only: [:assign_related_issues]
...@@ -348,4 +350,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -348,4 +350,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
access_denied! unless access_check access_denied! unless access_check
end end
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42441
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42438')
end
def whitelist_query_limiting_ee
# Also see https://gitlab.com/gitlab-org/gitlab-ee/issues/4793
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4792')
end
end end
...@@ -2,6 +2,7 @@ class Projects::NetworkController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath include ExtractsPath
include ApplicationHelper include ApplicationHelper
before_action :whitelist_query_limiting
before_action :require_non_empty_project before_action :require_non_empty_project
before_action :assign_ref_vars before_action :assign_ref_vars
before_action :authorize_download_code! before_action :authorize_download_code!
...@@ -35,4 +36,8 @@ class Projects::NetworkController < Projects::ApplicationController ...@@ -35,4 +36,8 @@ class Projects::NetworkController < Projects::ApplicationController
@options[:extended_sha1] = params[:extended_sha1] @options[:extended_sha1] = params[:extended_sha1]
@commit = @repo.commit(@options[:extended_sha1]) @commit = @repo.commit(@options[:extended_sha1])
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42333')
end
end end
...@@ -2,6 +2,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -2,6 +2,7 @@ class Projects::NotesController < Projects::ApplicationController
include NotesActions include NotesActions
include ToggleAwardEmoji include ToggleAwardEmoji
before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve] before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
...@@ -79,4 +80,8 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -79,4 +80,8 @@ class Projects::NotesController < Projects::ApplicationController
access_denied! unless can?(current_user, :create_note, noteable) access_denied! unless can?(current_user, :create_note, noteable)
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42383')
end
end end
class Projects::PipelinesController < Projects::ApplicationController class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts] before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :commit, only: [:show, :builds, :failures] before_action :commit, only: [:show, :builds, :failures]
before_action :authorize_read_pipeline! before_action :authorize_read_pipeline!
...@@ -166,4 +167,9 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -166,4 +167,9 @@ class Projects::PipelinesController < Projects::ApplicationController
def commit def commit
@commit ||= @pipeline.commit @commit ||= @pipeline.commit
end end
def whitelist_query_limiting
# Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42339')
end
end end
...@@ -4,6 +4,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -4,6 +4,7 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown include PreviewMarkdown
prepend EE::ProjectsController prepend EE::ProjectsController
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs] before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :redirect_git_extension, only: [:show] before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create] before_action :project, except: [:index, :new, :create]
...@@ -415,4 +416,8 @@ class ProjectsController < Projects::ApplicationController ...@@ -415,4 +416,8 @@ class ProjectsController < Projects::ApplicationController
# #
redirect_to request.original_url.sub(%r{\.git/?\Z}, '') if params[:format] == 'git' redirect_to request.original_url.sub(%r{\.git/?\Z}, '') if params[:format] == 'git'
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
end end
...@@ -2,6 +2,8 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -2,6 +2,8 @@ class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify include Recaptcha::Verify
prepend EE::RegistrationsController prepend EE::RegistrationsController
before_action :whitelist_query_limiting, only: [:destroy]
def new def new
redirect_to(new_user_session_path) redirect_to(new_user_session_path)
end end
...@@ -84,4 +86,8 @@ class RegistrationsController < Devise::RegistrationsController ...@@ -84,4 +86,8 @@ class RegistrationsController < Devise::RegistrationsController
def devise_mapping def devise_mapping
@devise_mapping ||= Devise.mappings[:user] @devise_mapping ||= Devise.mappings[:user]
end end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380')
end
end end
...@@ -87,20 +87,10 @@ module Storage ...@@ -87,20 +87,10 @@ module Storage
remove_exports! remove_exports!
end end
def remove_exports! def remove_legacy_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) legacy_export_path = File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def export_path
File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def full_path_was FileUtils.rm_rf(legacy_export_path)
if parent
parent.full_path + '/' + path_was
else
path_was
end
end end
end end
end end
...@@ -312,12 +312,6 @@ class Group < Namespace ...@@ -312,12 +312,6 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end end
def full_path_was
return path_was unless has_parent?
"#{parent.full_path}/#{path_was}"
end
def group_member(user) def group_member(user)
if group_members.loaded? if group_members.loaded?
group_members.find { |gm| gm.user_id == user.id } group_members.find { |gm| gm.user_id == user.id }
......
...@@ -235,6 +235,24 @@ class Namespace < ActiveRecord::Base ...@@ -235,6 +235,24 @@ class Namespace < ActiveRecord::Base
feature_available?(:multiple_issue_boards) feature_available?(:multiple_issue_boards)
end end
def full_path_was
return path_was unless has_parent?
"#{parent.full_path}/#{path_was}"
end
# Exports belonging to projects with legacy storage are placed in a common
# subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove
# them.
#
# Exports of projects using hashed storage are placed in a location defined
# only by the project ID, so each must be removed individually.
def remove_exports!
remove_legacy_exports!
all_projects.with_storage_feature(:repository).find_each(&:remove_exports)
end
private private
def refresh_access_of_projects_invited_groups def refresh_access_of_projects_invited_groups
......
...@@ -73,6 +73,7 @@ class Project < ActiveRecord::Base ...@@ -73,6 +73,7 @@ class Project < ActiveRecord::Base
before_destroy :remove_private_deploy_keys before_destroy :remove_private_deploy_keys
after_destroy -> { run_after_commit { remove_pages } } after_destroy -> { run_after_commit { remove_pages } }
after_destroy :remove_exports
after_validation :check_pending_delete after_validation :check_pending_delete
...@@ -1537,6 +1538,8 @@ class Project < ActiveRecord::Base ...@@ -1537,6 +1538,8 @@ class Project < ActiveRecord::Base
end end
def export_path def export_path
return nil unless namespace.present? || hashed_storage?(:repository)
File.join(Gitlab::ImportExport.storage_path, disk_path) File.join(Gitlab::ImportExport.storage_path, disk_path)
end end
...@@ -1545,8 +1548,9 @@ class Project < ActiveRecord::Base ...@@ -1545,8 +1548,9 @@ class Project < ActiveRecord::Base
end end
def remove_exports def remove_exports
_, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete)) return nil unless export_path.present?
status.zero?
FileUtils.rm_rf(export_path)
end end
def full_path_slug def full_path_slug
......
...@@ -91,6 +91,10 @@ module MergeRequests ...@@ -91,6 +91,10 @@ module MergeRequests
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id) UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
end end
# Upcoming method calls need the refreshed version of
# @source_merge_requests diffs (for MergeRequest#commit_shas for instance).
merge_requests_for_source_branch(reload: true)
end end
# Note: Closed merge requests also need approvals reset. # Note: Closed merge requests also need approvals reset.
...@@ -212,7 +216,8 @@ module MergeRequests ...@@ -212,7 +216,8 @@ module MergeRequests
merge_requests.uniq.select(&:source_project) merge_requests.uniq.select(&:source_project)
end end
def merge_requests_for_source_branch def merge_requests_for_source_branch(reload: false)
@source_merge_requests = nil if reload
@source_merge_requests ||= merge_requests_for(@branch_name) @source_merge_requests ||= merge_requests_for(@branch_name)
end end
......
class AttachmentUploader < GitlabUploader class AttachmentUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern include RecordsUploads::Concern
include ObjectStorage::Concern include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads prepend ObjectStorage::Extension::RecordsUploads
include UploaderHelper
private private
......
...@@ -8,11 +8,11 @@ class AvatarUploader < GitlabUploader ...@@ -8,11 +8,11 @@ class AvatarUploader < GitlabUploader
model.avatar.file && model.avatar.file.present? model.avatar.file && model.avatar.file.present?
end end
def move_to_store def move_to_cache
false false
end end
def move_to_cache def move_to_store
false false
end end
......
...@@ -15,8 +15,6 @@ class FileUploader < GitlabUploader ...@@ -15,8 +15,6 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)} MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)} DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
attr_accessor :model
def self.root def self.root
File.join(options.storage_path, 'uploads') File.join(options.storage_path, 'uploads')
end end
...@@ -62,6 +60,8 @@ class FileUploader < GitlabUploader ...@@ -62,6 +60,8 @@ class FileUploader < GitlabUploader
SecureRandom.hex SecureRandom.hex
end end
attr_accessor :model
def initialize(model, secret = nil) def initialize(model, secret = nil)
@model = model @model = model
@secret = secret @secret = secret
......
...@@ -45,6 +45,10 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -45,6 +45,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
file.present? file.present?
end end
def store_dir
File.join(base_dir, dynamic_segment)
end
def cache_dir def cache_dir
File.join(root, base_dir, 'tmp/cache') File.join(root, base_dir, 'tmp/cache')
end end
...@@ -62,10 +66,6 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -62,10 +66,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
# Designed to be overridden by child uploaders that have a dynamic path # Designed to be overridden by child uploaders that have a dynamic path
# segment -- that is, a path that changes based on mutable attributes of its # segment -- that is, a path that changes based on mutable attributes of its
# associated model # associated model
#
# For example, `FileUploader` builds the storage path based on the associated
# project model's `path_with_namespace` value, which can change when the
# project or its containing namespace is moved or renamed.
def dynamic_segment def dynamic_segment
raise(NotImplementedError) raise(NotImplementedError)
end end
......
.top-area .top-area
%ul.nav-links %ul.nav-links.mobile-separator
= nav_link(page: dashboard_groups_path) do = nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do = link_to dashboard_groups_path, title: _("Your groups") do
Your groups Your groups
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs .top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs %ul.nav-links.scrolling-tabs.mobile-separator
= nav_link(page: [dashboard_projects_path, root_path]) do = nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do = link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects Your projects
......
.nav-block .nav-block
%ul.nav-links %ul.nav-links.mobile-separator
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do = nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path = link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do = nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if current_user.todos.any? - if current_user.todos.any?
.top-area .top-area
%ul.nav-links %ul.nav-links.mobile-separator
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }> %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
.col-sm-7.brand-holder.pull-left .col-sm-7.brand-holder.pull-left
%h1 %h1
= brand_title = brand_title
= brand_image = brand_image
- if brand_item&.description? - if brand_item&.description?
= brand_text = brand_text
- else - else
......
%ul.nav-links %ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }> %li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do = link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All") = s_("PipelineSchedules|All")
......
- failed_builds = @pipeline.statuses.latest.failed - failed_builds = @pipeline.statuses.latest.failed
.tabs-holder .tabs-holder
%ul.pipelines-tabs.nav-links.no-top.no-bottom %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator
%li.js-pipeline-tab-link %li.js-pipeline-tab-link
= link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do = link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
Pipeline Pipeline
......
%ul.nav-links.event-filter.scrolling-tabs .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all') .fade-left= icon('angle-left')
- if event_filter_visible(:repository) .fade-right= icon('angle-right')
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events') %ul.nav-links.event-filter.scrolling-tabs
- if event_filter_visible(:merge_requests) = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
= event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events') - if event_filter_visible(:repository)
- if event_filter_visible(:issues) = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
= event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events') - if event_filter_visible(:merge_requests)
- if comments_visible? = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
= event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments') - if event_filter_visible(:issues)
= event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team') = event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events')
- if comments_visible?
= event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments')
= event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team')
%ul.nav-links %ul.nav-links.mobile-separator
%li{ class: milestone_class_for_state(params[:state], 'opened', true) }> %li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
= link_to milestones_filter_path(state: 'opened') do = link_to milestones_filter_path(state: 'opened') do
Open Open
......
%ul.nav-links %ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }> %li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do = link_to build_path_proc.call(nil) do
All All
......
- type = local_assigns.fetch(:type, :issues) - type = local_assigns.fetch(:type, :issues)
- page_context_word = type.to_s.humanize(capitalize: false) - page_context_word = type.to_s.humanize(capitalize: false)
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters.mobile-separator
%li{ class: active_when(params[:state] == 'opened') }> %li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened)} #{issuables_state_counter_text(type, :opened)}
......
- subject = local_assigns.fetch(:subject, current_user) - subject = local_assigns.fetch(:subject, current_user)
- include_private = local_assigns.fetch(:include_private, false) - include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu .nav-links.snippet-scope-menu.mobile-separator
%li{ class: active_when(params[:scope].nil?) } %li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do = link_to subject_snippets_path(subject) do
All All
......
---
title: FIx N+1 queries with /api/v4/groups endpoint
merge_request:
author:
type: performance
---
title: Add unique constraint to trending_projects#project_id.
merge_request: 16846
author:
type: other
---
title: Fix export removal for hashed-storage projects within a renamed or deleted
namespace
merge_request: 16658
author:
type: fixed
---
title: Fix GitLab import leaving group_id on ProjectLabel
merge_request: 16877
author:
type: fixed
---
title: Change button group width on mobile
merge_request: 16726
author: George Tsiolis
type: fixed
---
title: Reload MRs memoization after diffs creation
merge_request:
author:
type: fixed
---
title: Track and act upon the number of executed queries
merge_request:
author:
type: added
...@@ -346,7 +346,6 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values ...@@ -346,7 +346,6 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values
# Settings.artifact['path'] is deprecated, use `storage_path` instead # Settings.artifact['path'] is deprecated, use `storage_path` instead
Settings.artifacts['path'] = Settings.artifacts['storage_path'] Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings.artifacts['object_store'] ||= Settingslogic.new({}) Settings.artifacts['object_store'] ||= Settingslogic.new({})
Settings.artifacts['object_store']['enabled'] ||= false Settings.artifacts['object_store']['enabled'] ||= false
Settings.artifacts['object_store']['remote_directory'] ||= nil Settings.artifacts['object_store']['remote_directory'] ||= nil
...@@ -413,6 +412,13 @@ Settings.uploads['object_store']['background_upload'] ||= true ...@@ -413,6 +412,13 @@ Settings.uploads['object_store']['background_upload'] ||= true
# Convert upload connection settings to use string keys, to make Fog happy # Convert upload connection settings to use string keys, to make Fog happy
Settings.uploads['object_store']['connection']&.deep_stringify_keys! Settings.uploads['object_store']['connection']&.deep_stringify_keys!
#
# Uploads
#
Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
# #
# Mattermost # Mattermost
# #
......
if Gitlab::QueryLimiting.enable?
require_dependency 'gitlab/query_limiting/active_support_subscriber'
require_dependency 'gitlab/query_limiting/transaction'
require_dependency 'gitlab/query_limiting/middleware'
Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::QueryLimiting::Middleware)
end
end
class AddUniqueConstraintToTrendingProjectsProjectId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :trending_projects, :project_id, unique: true, name: 'index_trending_projects_on_project_id_unique'
remove_concurrent_index_by_name :trending_projects, 'index_trending_projects_on_project_id'
rename_index :trending_projects, 'index_trending_projects_on_project_id_unique', 'index_trending_projects_on_project_id'
end
def down
rename_index :trending_projects, 'index_trending_projects_on_project_id', 'index_trending_projects_on_project_id_old'
add_concurrent_index :trending_projects, :project_id
remove_concurrent_index_by_name :trending_projects, 'index_trending_projects_on_project_id_old'
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RemoveProjectLabelsGroupId < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
update_column_in_batches(:labels, :group_id, nil) do |table, query|
query.where(table[:type].eq('ProjectLabel').and(table[:group_id].not_eq(nil)))
end
end
def down
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180201145907) do ActiveRecord::Schema.define(version: 20180202111106) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1215,7 +1215,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do ...@@ -1215,7 +1215,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do
t.datetime "last_edited_at" t.datetime "last_edited_at"
t.integer "last_edited_by_id" t.integer "last_edited_by_id"
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.datetime "closed_at" t.datetime_with_timezone "closed_at"
end end
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
...@@ -2229,7 +2229,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do ...@@ -2229,7 +2229,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do
t.integer "project_id", null: false t.integer "project_id", null: false
end end
add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", unique: true, using: :btree
create_table "u2f_registrations", force: :cascade do |t| create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate" t.text "certificate"
......
...@@ -75,6 +75,7 @@ comments: false ...@@ -75,6 +75,7 @@ comments: false
- [Ordering table columns](ordering_table_columns.md) - [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md) - [Verifying database capabilities](verifying_database_capabilities.md)
- [Database Debugging and Troubleshooting](database_debugging.md) - [Database Debugging and Troubleshooting](database_debugging.md)
- [Query Count Limits](query_count_limits.md)
## Testing guides ## Testing guides
......
# Query Count Limits
Each controller or API endpoint is allowed to execute up to 100 SQL queries. In
a production environment we'll only log an error in case this threshold is
exceeded, but in a test environment we'll raise an error instead.
## Solving Failing Tests
When a test fails because it executes more than 100 SQL queries there are two
solutions to this problem:
1. Reduce the number of SQL queries that are executed.
2. Whitelist the controller or API endpoint.
You should only resort to whitelisting when an existing controller or endpoint
is to blame as in this case reducing the number of SQL queries can take a lot of
effort. Newly added controllers and endpoints are not allowed to execute more
than 100 SQL queries and no exceptions will be made for this rule. _If_ a large
number of SQL queries is necessary to perform certain work it's best to have
this work performed by Sidekiq instead of doing this directly in a web request.
## Whitelisting
In the event that you _have_ to whitelist a controller you'll first need to
create an issue. This issue should (preferably in the title) mention the
controller or endpoint and include the appropriate labels (`database`,
`performance`, and at least a team specific label such as `Discussion`).
Once the issue has been created you can whitelist the code in question. For
Rails controllers it's best to create a `before_action` hook that runs as early
as possible. The called method in turn should call
`Gitlab::QueryLimiting.whitelist('issue URL here')`. For example:
```ruby
class MyController < ApplicationController
before_action :whitelist_query_limiting, only: [:show]
def index
# ...
end
def show
# ...
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/...')
end
end
```
By using a `before_action` you don't have to modify the controller method in
question, reducing the likelihood of merge conflicts.
For Grape API endpoints there unfortunately is not a reliable way of running a
hook before a specific endpoint. This means that you have to add the whitelist
call directly into the endpoint like so:
```ruby
get '/projects/:id/foo' do
Gitlab::QueryLimiting.whitelist('...')
# ...
end
```
module EE
module API
module APIGuard
module HelperMethods
extend ::Gitlab::Utils::Override
override :find_user_from_sources
def find_user_from_sources
find_user_from_access_token ||
find_user_from_job_token ||
find_user_from_warden
end
end
end
end
end
module Gitlab module Gitlab
module Geo module Geo
class JobArtifactUploader < FileUploader class JobArtifactUploader < ::Gitlab::Geo::FileUploader
def execute def execute
job_artifact = ::Ci::JobArtifact.find_by(id: object_db_id) job_artifact = ::Ci::JobArtifact.find_by(id: object_db_id)
......
...@@ -39,10 +39,11 @@ module API ...@@ -39,10 +39,11 @@ module API
# Helper Methods for Grape Endpoint # Helper Methods for Grape Endpoint
module HelperMethods module HelperMethods
prepend EE::API::APIGuard::HelperMethods
include Gitlab::Auth::UserAuthFinders include Gitlab::Auth::UserAuthFinders
def find_current_user! def find_current_user!
user = find_user_from_access_token || find_user_from_job_token || find_user_from_warden user = find_user_from_sources
return unless user return unless user
forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api) forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
...@@ -50,6 +51,10 @@ module API ...@@ -50,6 +51,10 @@ module API
user user
end end
def find_user_from_sources
find_user_from_access_token || find_user_from_warden
end
private private
# An array of scopes that were registered (using `allow_access_with_scope`) # An array of scopes that were registered (using `allow_access_with_scope`)
......
...@@ -29,6 +29,8 @@ module API ...@@ -29,6 +29,8 @@ module API
use :pagination use :pagination
end end
get ':id/repository/branches' do get ':id/repository/branches' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42329')
repository = user_project.repository repository = user_project.repository
branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name)) branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name))
merged_branch_names = repository.merged_branch_names(branches.map(&:name)) merged_branch_names = repository.merged_branch_names(branches.map(&:name))
......
...@@ -54,6 +54,8 @@ module API ...@@ -54,6 +54,8 @@ module API
find_params[:parent] = find_group!(params[:id]) if params[:id] find_params[:parent] = find_group!(params[:id]) if params[:id]
groups = GroupsFinder.new(current_user, find_params).execute groups = GroupsFinder.new(current_user, find_params).execute
# EE-only
groups = groups.preload(:ldap_group_links)
groups = groups.search(params[:search]) if params[:search].present? groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort]) groups = groups.reorder(params[:order_by] => params[:sort])
...@@ -181,6 +183,8 @@ module API ...@@ -181,6 +183,8 @@ module API
desc 'Remove a group.' desc 'Remove a group.'
delete ":id" do delete ":id" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4795')
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
......
...@@ -166,6 +166,8 @@ module API ...@@ -166,6 +166,8 @@ module API
use :issue_params use :issue_params
end end
post ':id/issues' do post ':id/issues' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42320')
authorize! :create_issue, user_project authorize! :create_issue, user_project
# Setting created_at time only allowed for admins and project owners # Setting created_at time only allowed for admins and project owners
...@@ -207,6 +209,8 @@ module API ...@@ -207,6 +209,8 @@ module API
:weight, :discussion_locked :weight, :discussion_locked
end end
put ':id/issues/:issue_iid' do put ':id/issues/:issue_iid' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42322')
issue = user_project.issues.find_by!(iid: params.delete(:issue_iid)) issue = user_project.issues.find_by!(iid: params.delete(:issue_iid))
authorize! :update_issue, issue authorize! :update_issue, issue
...@@ -240,6 +244,8 @@ module API ...@@ -240,6 +244,8 @@ module API
requires :to_project_id, type: Integer, desc: 'The ID of the new project' requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end end
post ':id/issues/:issue_iid/move' do post ':id/issues/:issue_iid/move' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42323')
issue = user_project.issues.find_by(iid: params[:issue_iid]) issue = user_project.issues.find_by(iid: params[:issue_iid])
not_found!('Issue') unless issue not_found!('Issue') unless issue
......
...@@ -164,6 +164,8 @@ module API ...@@ -164,6 +164,8 @@ module API
use :optional_params use :optional_params
end end
post ":id/merge_requests" do post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42316')
authorize! :create_merge_request, user_project authorize! :create_merge_request, user_project
mr_params = declared_params(include_missing: false) mr_params = declared_params(include_missing: false)
...@@ -273,6 +275,8 @@ module API ...@@ -273,6 +275,8 @@ module API
at_least_one_of(*(at_least_one_of_ce + at_least_one_of_ee)) at_least_one_of(*(at_least_one_of_ce + at_least_one_of_ee))
end end
put ':id/merge_requests/:merge_request_iid' do put ':id/merge_requests/:merge_request_iid' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42318')
merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request) merge_request = find_merge_request_with_access(params.delete(:merge_request_iid), :update_merge_request)
mr_params = declared_params(include_missing: false) mr_params = declared_params(include_missing: false)
...@@ -303,6 +307,8 @@ module API ...@@ -303,6 +307,8 @@ module API
optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end end
put ':id/merge_requests/:merge_request_iid/merge' do put ':id/merge_requests/:merge_request_iid/merge' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42317')
merge_request = find_project_merge_request(params[:merge_request_iid]) merge_request = find_project_merge_request(params[:merge_request_iid])
merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds]) merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
......
...@@ -42,6 +42,8 @@ module API ...@@ -42,6 +42,8 @@ module API
requires :ref, type: String, desc: 'Reference' requires :ref, type: String, desc: 'Reference'
end end
post ':id/pipeline' do post ':id/pipeline' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42124')
authorize! :create_pipeline, user_project authorize! :create_pipeline, user_project
new_pipeline = Ci::CreatePipelineService.new(user_project, new_pipeline = Ci::CreatePipelineService.new(user_project,
......
...@@ -216,6 +216,8 @@ module API ...@@ -216,6 +216,8 @@ module API
optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into'
end end
post ':id/fork' do post ':id/fork' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42284')
fork_params = declared_params(include_missing: false) fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace] namespace_id = fork_params[:namespace]
......
...@@ -15,6 +15,8 @@ module API ...@@ -15,6 +15,8 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build' optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42283')
# validate variables # validate variables
params[:variables] = params[:variables].to_h params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
......
...@@ -389,6 +389,8 @@ module API ...@@ -389,6 +389,8 @@ module API
optional :hard_delete, type: Boolean, desc: "Whether to remove a user's contributions" optional :hard_delete, type: Boolean, desc: "Whether to remove a user's contributions"
end end
delete ":id" do delete ":id" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42279')
authenticated_as_admin! authenticated_as_admin!
user = User.find_by(id: params[:id]) user = User.find_by(id: params[:id])
......
...@@ -14,6 +14,8 @@ module API ...@@ -14,6 +14,8 @@ module API
success ::API::Entities::Branch success ::API::Entities::Branch
end end
get ":id/repository/branches" do get ":id/repository/branches" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276')
repository = user_project.repository repository = user_project.repository
branches = repository.branches.sort_by(&:name) branches = repository.branches.sort_by(&:name)
merged_branch_names = repository.merged_branch_names(branches.map(&:name)) merged_branch_names = repository.merged_branch_names(branches.map(&:name))
......
...@@ -151,6 +151,8 @@ module API ...@@ -151,6 +151,8 @@ module API
desc 'Remove a group.' desc 'Remove a group.'
delete ":id" do delete ":id" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4797')
group = find_group!(params[:id]) group = find_group!(params[:id])
authorize! :admin_group, group authorize! :admin_group, group
present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
......
...@@ -135,6 +135,8 @@ module API ...@@ -135,6 +135,8 @@ module API
use :issue_params use :issue_params
end end
post ':id/issues' do post ':id/issues' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42131')
# Setting created_at time only allowed for admins and project owners # Setting created_at time only allowed for admins and project owners
unless current_user.admin? || user_project.owner == current_user unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at) params.delete(:created_at)
...@@ -171,6 +173,8 @@ module API ...@@ -171,6 +173,8 @@ module API
:weight :weight
end end
put ':id/issues/:issue_id' do put ':id/issues/:issue_id' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42132')
issue = user_project.issues.find(params.delete(:issue_id)) issue = user_project.issues.find(params.delete(:issue_id))
authorize! :update_issue, issue authorize! :update_issue, issue
...@@ -203,6 +207,8 @@ module API ...@@ -203,6 +207,8 @@ module API
requires :to_project_id, type: Integer, desc: 'The ID of the new project' requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end end
post ':id/issues/:issue_id/move' do post ':id/issues/:issue_id/move' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42133')
issue = user_project.issues.find_by(id: params[:issue_id]) issue = user_project.issues.find_by(id: params[:issue_id])
not_found!('Issue') unless issue not_found!('Issue') unless issue
......
...@@ -93,6 +93,8 @@ module API ...@@ -93,6 +93,8 @@ module API
use :optional_params use :optional_params
end end
post ":id/merge_requests" do post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126')
authorize! :create_merge_request, user_project authorize! :create_merge_request, user_project
mr_params = declared_params(include_missing: false) mr_params = declared_params(include_missing: false)
...@@ -169,6 +171,8 @@ module API ...@@ -169,6 +171,8 @@ module API
:remove_source_branch, :squash :remove_source_branch, :squash
end end
put path do put path do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42127')
merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request)
mr_params = declared_params(include_missing: false) mr_params = declared_params(include_missing: false)
...@@ -196,6 +200,8 @@ module API ...@@ -196,6 +200,8 @@ module API
optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge' optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end end
put "#{path}/merge" do put "#{path}/merge" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4796')
merge_request = find_project_merge_request(params[:merge_request_id]) merge_request = find_project_merge_request(params[:merge_request_id])
# Merge request can not be merged # Merge request can not be merged
......
...@@ -19,6 +19,8 @@ module API ...@@ -19,6 +19,8 @@ module API
desc: 'Either running, branches, or tags' desc: 'Either running, branches, or tags'
end end
get ':id/pipelines' do get ':id/pipelines' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123')
authorize! :read_pipeline, user_project authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute
......
...@@ -16,6 +16,8 @@ module API ...@@ -16,6 +16,8 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build' optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end end
post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121')
# validate variables # validate variables
params[:variables] = params[:variables].to_h params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) } unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
......
...@@ -1222,33 +1222,13 @@ module Gitlab ...@@ -1222,33 +1222,13 @@ module Gitlab
end end
def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:) def squash(user, squash_id, branch:, start_sha:, end_sha:, author:, message:)
squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id) gitaly_migrate(:squash) do |is_enabled|
env = git_env_for_user(user).merge( if is_enabled
'GIT_AUTHOR_NAME' => author.name, gitaly_operation_client.user_squash(user, squash_id, branch,
'GIT_AUTHOR_EMAIL' => author.email start_sha, end_sha, author, message)
) else
diff_range = "#{start_sha}...#{end_sha}" git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
diff_files = run_git!(
%W(diff --name-only --diff-filter=a --binary #{diff_range})
).chomp
with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
# Apply diff of the `diff_range` to the worktree
diff = run_git!(%W(diff --binary #{diff_range}))
run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
stdin.write(diff)
end end
# Commit the `diff_range` diff
run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
# Return the squash sha. May print a warning for ambiguous refs, but
# we can ignore that with `--quiet` and just take the SHA, if present.
# HEAD here always refers to the current HEAD commit, even if there is
# another ref called HEAD.
run_git!(
%w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
).chomp
end end
end end
...@@ -2164,6 +2144,37 @@ module Gitlab ...@@ -2164,6 +2144,37 @@ module Gitlab
end end
end end
def git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
squash_path = worktree_path(SQUASH_WORKTREE_PREFIX, squash_id)
env = git_env_for_user(user).merge(
'GIT_AUTHOR_NAME' => author.name,
'GIT_AUTHOR_EMAIL' => author.email
)
diff_range = "#{start_sha}...#{end_sha}"
diff_files = run_git!(
%W(diff --name-only --diff-filter=a --binary #{diff_range})
).chomp
with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
# Apply diff of the `diff_range` to the worktree
diff = run_git!(%W(diff --binary #{diff_range}))
run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
stdin.write(diff)
end
# Commit the `diff_range` diff
run_git!(%W(commit --no-verify --message #{message}), chdir: squash_path, env: env)
# Return the squash sha. May print a warning for ambiguous refs, but
# we can ignore that with `--quiet` and just take the SHA, if present.
# HEAD here always refers to the current HEAD commit, even if there is
# another ref called HEAD.
run_git!(
%w(rev-parse --quiet --verify HEAD), chdir: squash_path, env: env
).chomp
end
end
def local_fetch_ref(source_path, source_ref:, target_ref:) def local_fetch_ref(source_path, source_ref:, target_ref:)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args) run_git(args)
......
...@@ -183,6 +183,32 @@ module Gitlab ...@@ -183,6 +183,32 @@ module Gitlab
end end
end end
def user_squash(user, squash_id, branch, start_sha, end_sha, author, message)
request = Gitaly::UserSquashRequest.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
squash_id: squash_id.to_s,
branch: encode_binary(branch),
start_sha: start_sha,
end_sha: end_sha,
author: Gitlab::Git::User.from_gitlab(author).to_gitaly,
commit_message: encode_binary(message)
)
response = GitalyClient.call(
@repository.storage,
:operation_service,
:user_squash,
request
)
if response.git_error.presence
raise Gitlab::Git::Repository::GitError, response.git_error
end
response.squash_sha
end
def user_commit_files( def user_commit_files(
user, branch_name, commit_message, actions, author_email, author_name, user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository) start_branch_name, start_repository)
......
...@@ -139,13 +139,12 @@ module Gitlab ...@@ -139,13 +139,12 @@ module Gitlab
end end
def setup_label def setup_label
return unless @relation_hash['type'] == 'GroupLabel'
# If there's no group, move the label to a project label # If there's no group, move the label to a project label
if @relation_hash['group_id'] if @relation_hash['type'] == 'GroupLabel' && @relation_hash['group_id']
@relation_hash['project_id'] = nil @relation_hash['project_id'] = nil
@relation_name = :group_label @relation_name = :group_label
else else
@relation_hash['group_id'] = nil
@relation_hash['type'] = 'ProjectLabel' @relation_hash['type'] = 'ProjectLabel'
end end
end end
......
module Gitlab
module QueryLimiting
# Returns true if we should enable tracking of query counts.
#
# This is only enabled in production/staging if we're running on GitLab.com.
# This ensures we don't produce any errors that users can't do anything
# about themselves.
def self.enable?
Gitlab.com? || Rails.env.development? || Rails.env.test?
end
# Allows the current request to execute any number of SQL queries.
#
# This method should _only_ be used when there's a corresponding issue to
# reduce the number of queries.
#
# The issue URL is only meant to push developers into creating an issue
# instead of blindly whitelisting offending blocks of code.
def self.whitelist(issue_url)
return unless enable_whitelist?
unless issue_url.start_with?('https://')
raise(
ArgumentError,
'You must provide a valid issue URL in order to whitelist a block of code'
)
end
Transaction&.current&.whitelisted = true
end
def self.enable_whitelist?
Rails.env.development? || Rails.env.test?
end
end
end
module Gitlab
module QueryLimiting
class ActiveSupportSubscriber < ActiveSupport::Subscriber
attach_to :active_record
def sql(*)
Transaction.current&.increment
end
end
end
end
# frozen_string_literal: true
module Gitlab
module QueryLimiting
# Middleware for reporting (or raising) when a request performs more than a
# certain amount of database queries.
class Middleware
CONTROLLER_KEY = 'action_controller.instance'.freeze
ENDPOINT_KEY = 'api.endpoint'.freeze
def initialize(app)
@app = app
end
def call(env)
transaction, retval = Transaction.run do
@app.call(env)
end
transaction.action = action_name(env)
transaction.act_upon_results
retval
end
def action_name(env)
if env[CONTROLLER_KEY]
action_for_rails(env)
elsif env[ENDPOINT_KEY]
action_for_grape(env)
end
end
private
def action_for_rails(env)
controller = env[CONTROLLER_KEY]
action = "#{controller.class.name}##{controller.action_name}"
if controller.content_type == 'text/html'
action
else
"#{action} (#{controller.content_type})"
end
end
def action_for_grape(env)
endpoint = env[ENDPOINT_KEY]
route = endpoint.route rescue nil
"#{route.request_method} #{route.path}" if route
end
end
end
end
module Gitlab
module QueryLimiting
class Transaction
THREAD_KEY = :__gitlab_query_counts_transaction
attr_accessor :count, :whitelisted
# The name of the action (e.g. `UsersController#show`) that is being
# executed.
attr_accessor :action
# The maximum number of SQL queries that can be executed in a request. For
# the sake of keeping things simple we hardcode this value here, it's not
# supposed to be changed very often anyway.
THRESHOLD = 100
# Error that is raised whenever exceeding the maximum number of queries.
ThresholdExceededError = Class.new(StandardError)
def self.current
Thread.current[THREAD_KEY]
end
# Starts a new transaction and returns it and the blocks' return value.
#
# Example:
#
# transaction, retval = Transaction.run do
# 10
# end
#
# retval # => 10
def self.run
transaction = new
Thread.current[THREAD_KEY] = transaction
[transaction, yield]
ensure
Thread.current[THREAD_KEY] = nil
end
def initialize
@action = nil
@count = 0
@whitelisted = false
end
# Sends a notification based on the number of executed SQL queries.
def act_upon_results
return unless threshold_exceeded?
error = ThresholdExceededError.new(error_message)
if raise_error?
raise(error)
else
# Raven automatically logs to the Rails log if disabled, thus we don't
# need to manually log anything in case Sentry support is not enabled.
Raven.capture_exception(error)
end
end
def increment
@count += 1 unless whitelisted
end
def raise_error?
Rails.env.test?
end
def threshold_exceeded?
count > THRESHOLD
end
def error_message
header = 'Too many SQL queries were executed'
header += " in #{action}" if action
"#{header}: a maximum of #{THRESHOLD} is allowed but #{count} SQL queries were executed"
end
end
end
end
...@@ -2,7 +2,7 @@ desc 'Code duplication analyze via flay' ...@@ -2,7 +2,7 @@ desc 'Code duplication analyze via flay'
task :flay do task :flay do
output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}` output = `bundle exec flay --mass 35 app/ lib/gitlab/ 2> #{File::NULL}`
if output.include? "Similar code found" if output.include?("Similar code found") || output.include?("IDENTICAL code found")
puts output puts output
exit 1 exit 1
end end
......
...@@ -125,6 +125,12 @@ FactoryBot.define do ...@@ -125,6 +125,12 @@ FactoryBot.define do
avatar { fixture_file_upload('spec/fixtures/dk.png') } avatar { fixture_file_upload('spec/fixtures/dk.png') }
end end
trait :with_export do
after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
end
trait :broken_storage do trait :broken_storage do
after(:create) do |project| after(:create) do |project|
project.update_column(:repository_storage, 'broken') project.update_column(:repository_storage, 'broken')
......
require 'spec_helper' require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', :js do feature 'Import/Export - Namespace export file cleanup', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:project) { create(:project) } before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end end
after do after do
FileUtils.rm_rf(export_path, secure: true) FileUtils.rm_rf(export_path, secure: true)
end end
context 'admin user' do shared_examples_for 'handling project exports on namespace change' do
let!(:old_export_path) { project.export_path }
before do before do
sign_in(create(:admin)) sign_in(create(:admin))
setup_export_project
end end
context 'moving the namespace' do context 'moving the namespace' do
scenario 'removes the export file' do it 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
expect(File).to exist(old_export_path) expect(File).to exist(old_export_path)
project.namespace.update(path: 'new_path') project.namespace.update!(path: build(:namespace).path)
expect(File).not_to exist(old_export_path) expect(File).not_to exist(old_export_path)
end end
end end
context 'deleting the namespace' do context 'deleting the namespace' do
scenario 'removes the export file' do it 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
expect(File).to exist(old_export_path) expect(File).to exist(old_export_path)
project.namespace.destroy project.namespace.destroy
...@@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do ...@@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(File).not_to exist(old_export_path) expect(File).not_to exist(old_export_path)
end end
end end
end
def setup_export_project describe 'legacy storage' do
visit edit_project_path(project) let(:project) { create(:project) }
expect(page).to have_content('Export project') it_behaves_like 'handling project exports on namespace change'
end
describe 'hashed storage' do
let(:project) { create(:project, :hashed) }
find(:link, 'Export project').send_keys(:return) it_behaves_like 'handling project exports on namespace change'
end
visit edit_project_path(project) def setup_export_project
visit edit_project_path(project)
expect(page).to have_content('Download export') expect(page).to have_content('Export project')
end
find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
expect(page).to have_content('Download export')
end end
end end
/* global BoardService */ /* global BoardService */
import Vue from 'vue'; import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/labels_select'; import '~/labels_select';
import LabelsSelect from '~/boards/components/labels_select.vue'; import LabelsSelect from '~/boards/components/labels_select.vue';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
...@@ -32,14 +34,16 @@ const label2 = { ...@@ -32,14 +34,16 @@ const label2 = {
}; };
describe('LabelsSelect', () => { describe('LabelsSelect', () => {
let mock;
beforeEach((done) => { beforeEach((done) => {
setFixtures('<div class="test-container"></div>'); setFixtures('<div class="test-container"></div>');
const deferred = new jQuery.Deferred(); mock = new MockAdapter(axios);
spyOn($, 'ajax').and.returnValue(deferred.resolve([ mock.onGet('/some/path').reply(200, [
label, label,
label2, label2,
])); ]);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new IssuableContext(); new IssuableContext();
...@@ -60,6 +64,10 @@ describe('LabelsSelect', () => { ...@@ -60,6 +64,10 @@ describe('LabelsSelect', () => {
Vue.nextTick(done); Vue.nextTick(done);
}); });
afterEach(() => {
mock.restore();
});
describe('canEdit', () => { describe('canEdit', () => {
it('hides Edit button', (done) => { it('hides Edit button', (done) => {
vm.canEdit = false; vm.canEdit = false;
......
/* global BoardService */ /* global BoardService */
import Vue from 'vue'; import Vue from 'vue';
import MockAdapater from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import MilestoneSelect from '~/boards/components/milestone_select.vue'; import MilestoneSelect from '~/boards/components/milestone_select.vue';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
import { boardObj } from './mock_data'; import { boardObj } from './mock_data';
...@@ -92,12 +94,18 @@ describe('Milestone select component', () => { ...@@ -92,12 +94,18 @@ describe('Milestone select component', () => {
}); });
describe('clicking dropdown items', () => { describe('clicking dropdown items', () => {
let mock;
beforeEach(() => { beforeEach(() => {
const deferred = new jQuery.Deferred(); mock = new MockAdapater(axios);
spyOn($, 'ajax').and.returnValue(deferred.resolve([ mock.onGet('/test/issue-boards/milestones.json').reply(200, [
milestone, milestone,
milestone2, milestone2,
])); ]);
});
afterEach(() => {
mock.restore();
}); });
it('sets Any Milestone', (done) => { it('sets Any Milestone', (done) => {
......
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */ /* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Issue from '~/issue'; import Issue from '~/issue';
import '~/lib/utils/text_utility'; import '~/lib/utils/text_utility';
...@@ -88,20 +90,24 @@ describe('Issue', function() { ...@@ -88,20 +90,24 @@ describe('Issue', function() {
[true, false].forEach((isIssueInitiallyOpen) => { [true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() { describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
const action = isIssueInitiallyOpen ? 'close' : 'reopen'; const action = isIssueInitiallyOpen ? 'close' : 'reopen';
let mock;
function ajaxSpy(req) { function mockCloseButtonResponseSuccess(url, response) {
if (req.url === this.$triggeredButton.attr('href')) { mock.onPut(url).reply(() => {
expect(req.type).toBe('PUT');
expectNewBranchButtonState(true, false); expectNewBranchButtonState(true, false);
return this.issueStateDeferred;
} else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
expect(req.type).toBe('GET');
expectNewBranchButtonState(true, false);
return this.canCreateBranchDeferred;
}
expect(req.url).toBe('unexpected'); return [200, response];
return null; });
}
function mockCloseButtonResponseError(url) {
mock.onPut(url).networkError();
}
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
});
} }
beforeEach(function() { beforeEach(function() {
...@@ -111,6 +117,11 @@ describe('Issue', function() { ...@@ -111,6 +117,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw'); loadFixtures('issues/closed-issue.html.raw');
} }
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
findElements(isIssueInitiallyOpen); findElements(isIssueInitiallyOpen);
this.issue = new Issue(); this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen); expectIssueState(isIssueInitiallyOpen);
...@@ -120,71 +131,89 @@ describe('Issue', function() { ...@@ -120,71 +131,89 @@ describe('Issue', function() {
this.$projectIssuesCounter = $('.issue_counter').first(); this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001'); this.$projectIssuesCounter.text('1,001');
this.issueStateDeferred = new jQuery.Deferred(); spyOn(axios, 'get').and.callThrough();
this.canCreateBranchDeferred = new jQuery.Deferred(); });
spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this)); afterEach(() => {
mock.restore();
$('div.flash-alert').remove();
}); });
it(`${action}s the issue`, function() { it(`${action}s the issue`, function(done) {
this.$triggeredButton.trigger('click'); mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
this.issueStateDeferred.resolve({
id: 34 id: 34
}); });
this.canCreateBranchDeferred.resolve({ mockCanCreateBranch(!isIssueInitiallyOpen);
can_create_branch: !isIssueInitiallyOpen
});
expectIssueState(!isIssueInitiallyOpen); this.$triggeredButton.trigger('click');
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002'); setTimeout(() => {
expectNewBranchButtonState(false, !isIssueInitiallyOpen); expectIssueState(!isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
expectNewBranchButtonState(false, !isIssueInitiallyOpen);
done();
});
}); });
it(`fails to ${action} the issue if saved:false`, function() { it(`fails to ${action} the issue if saved:false`, function(done) {
this.$triggeredButton.trigger('click'); mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
this.issueStateDeferred.resolve({
saved: false saved: false
}); });
this.canCreateBranchDeferred.resolve({ mockCanCreateBranch(isIssueInitiallyOpen);
can_create_branch: isIssueInitiallyOpen
});
expectIssueState(isIssueInitiallyOpen); this.$triggeredButton.trigger('click');
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage(); setTimeout(() => {
expect(this.$projectIssuesCounter.text()).toBe('1,001'); expectIssueState(isIssueInitiallyOpen);
expectNewBranchButtonState(false, isIssueInitiallyOpen); expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
done();
});
}); });
it(`fails to ${action} the issue if HTTP error occurs`, function() { it(`fails to ${action} the issue if HTTP error occurs`, function(done) {
mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
mockCanCreateBranch(isIssueInitiallyOpen);
this.$triggeredButton.trigger('click'); this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
this.canCreateBranchDeferred.resolve({
can_create_branch: isIssueInitiallyOpen
});
expectIssueState(isIssueInitiallyOpen); setTimeout(() => {
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull(); expectIssueState(isIssueInitiallyOpen);
expectErrorMessage(); expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe('1,001'); expectErrorMessage();
expectNewBranchButtonState(false, isIssueInitiallyOpen); expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
done();
});
}); });
it('disables the new branch button if Ajax call fails', function() { it('disables the new branch button if Ajax call fails', function() {
mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
mock.onGet(/(.*)\/can_create_branch$/).networkError();
this.$triggeredButton.trigger('click'); this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
this.canCreateBranchDeferred.reject();
expectNewBranchButtonState(false, false); expectNewBranchButtonState(false, false);
}); });
it('does not trigger Ajax call if new branch button is missing', function() { it('does not trigger Ajax call if new branch button is missing', function(done) {
mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
Issue.$btnNewBranch = $(); Issue.$btnNewBranch = $();
this.canCreateBranchDeferred = null; this.canCreateBranchDeferred = null;
this.$triggeredButton.trigger('click'); this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
setTimeout(() => {
expect(axios.get).not.toHaveBeenCalled();
done();
});
}); });
}); });
}); });
......
This diff is collapsed.
/* eslint-disable no-new */ /* eslint-disable no-new */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context'; import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
...@@ -10,35 +12,44 @@ import '~/users_select'; ...@@ -10,35 +12,44 @@ import '~/users_select';
(() => { (() => {
let saveLabelCount = 0; let saveLabelCount = 0;
let mock;
describe('Issue dropdown sidebar', () => { describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html.raw'); preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => { beforeEach(() => {
loadFixtures('static/issue_sidebar_label.html.raw'); loadFixtures('static/issue_sidebar_label.html.raw');
mock = new MockAdapter(axios);
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}'); new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect(); new LabelsSelect();
spyOn(jQuery, 'ajax').and.callFake((req) => { mock.onGet('/root/test/labels.json').reply(() => {
const d = $.Deferred(); const labels = Array(10).fill().map((_, i) => ({
let LABELS_DATA = []; id: i,
title: `test ${i}`,
color: '#5CB85C',
}));
if (req.url === '/root/test/labels.json') { return [200, labels];
for (let i = 0; i < 10; i += 1) { });
LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
} mock.onPut('/root/test/issues/2.json').reply(() => {
} else if (req.url === '/root/test/issues/2.json') { const labels = Array(saveLabelCount).fill().map((_, i) => ({
const tmp = []; id: i,
for (let i = 0; i < saveLabelCount; i += 1) { title: `test ${i}`,
tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' }); color: '#5CB85C',
} }));
LABELS_DATA = { labels: tmp };
}
d.resolve(LABELS_DATA); return [200, { labels }];
return d.promise();
}); });
}); });
afterEach(() => {
mock.restore();
});
it('changes collapsed tooltip when changing labels when less than 5', (done) => { it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5; saveLabelCount = 5;
$('.edit-link').get(0).click(); $('.edit-link').get(0).click();
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment