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
lib/gitlab/gitaly_client/operation_service.rb
app/models/project_services/packagist_service.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
gem 'scss_lint', '~> 0.56.0', require: false
gem 'haml_lint', '~> 0.26.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 'benchmark-ips', '~> 2.3.0', require: false
......
......@@ -235,7 +235,7 @@ GEM
fast_gettext (1.4.0)
ffaker (2.4.0)
ffi (1.9.18)
flay (2.8.1)
flay (2.10.0)
erubis (~> 2.7.0)
path_expander (~> 1.0)
ruby_parser (~> 3.0)
......@@ -617,7 +617,7 @@ GEM
ast (~> 2.3)
parslet (1.5.0)
blankslate (~> 2.0)
path_expander (1.0.1)
path_expander (1.0.2)
peek (1.0.1)
concurrent-ruby (>= 0.9.0)
concurrent-ruby-ext (>= 0.9.0)
......@@ -1072,7 +1072,7 @@ DEPENDENCIES
faraday_middleware-aws-signers-v4
fast_blank
ffaker (~> 2.4)
flay (~> 2.8.0)
flay (~> 2.10.0)
flipper (~> 0.11.0)
flipper-active_record (~> 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 */
import 'vendor/jquery.waitforimages';
import axios from './lib/utils/axios_utils';
import { addDelimiter } from './lib/utils/text_utility';
import Flash from './flash';
import flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
......@@ -42,12 +43,8 @@ export default class Issue {
this.disableCloseReopenButton($button);
url = $button.attr('href');
return $.ajax({
type: 'PUT',
url: url
})
.fail(() => new Flash(issueFailMessage))
.done((data) => {
return axios.put(url)
.then(({ data }) => {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
const projectIssuesCounter = $('.issue_counter');
......@@ -74,9 +71,10 @@ export default class Issue {
}
}
} else {
new Flash(issueFailMessage);
flash(issueFailMessage);
}
})
.catch(() => flash(issueFailMessage))
.then(() => {
this.disableCloseReopenButton($button, false);
});
......@@ -115,24 +113,22 @@ export default class Issue {
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
return $.getJSON($container.data('url')).fail(function() {
return new Flash('Failed to load referenced merge requests');
}).done(function(data) {
if ('html' in data) {
return $container.html(data.html);
}
});
return axios.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {
$container.html(data.html);
}
}).catch(() => flash('Failed to load referenced merge requests'));
}
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
return $.getJSON($container.data('url')).fail(function() {
return new Flash('Failed to load related branches');
}).done(function(data) {
if ('html' in data) {
return $container.html(data.html);
}
});
return axios.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {
$container.html(data.html);
}
}).catch(() => flash('Failed to load related branches'));
}
}
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { visitUrl } from './lib/utils/url_utility';
import bp from './breakpoints';
import { numberToHumanSize } from './lib/utils/number_utils';
......@@ -8,6 +9,7 @@ export default class Job {
constructor(options) {
this.timeout = null;
this.state = null;
this.fetchingStatusFavicon = false;
this.options = options || $('.js-build-options').data();
this.pagePath = this.options.pagePath;
......@@ -171,12 +173,23 @@ export default class Job {
}
getBuildTrace() {
return $.ajax({
url: `${this.pagePath}/trace.json`,
data: { state: this.state },
return axios.get(`${this.pagePath}/trace.json`, {
params: { state: this.state },
})
.done((log) => {
setCiStatusFavicon(`${this.pagePath}/status.json`);
.then((res) => {
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) {
this.state = log.state;
......@@ -217,7 +230,7 @@ export default class Job {
visitUrl(this.pagePath);
}
})
.fail(() => {
.catch(() => {
this.$buildRefreshAnimation.remove();
})
.then(() => {
......
......@@ -2,9 +2,12 @@
/* global Issuable */
/* global ListLabel */
import _ from 'underscore';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import DropdownUtils from './filtered_search/dropdown_utils';
import CreateLabelDropdown from './create_label';
import flash from './flash';
export default class LabelsSelect {
constructor(els, options = {}) {
......@@ -82,99 +85,96 @@ export default class LabelsSelect {
}
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
dataType: 'JSON',
data: data
}).done(function(data) {
var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueURLSplit = issueURLSplit;
labelCount = 0;
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
}
else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
axios.put(issueUpdateURL, data)
.then(({ data }) => {
var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
data.issueURLSplit = issueURLSplit;
labelCount = 0;
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
}
else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
if (data.labels.length) {
labelTitles = data.labels.map(function(label) {
return label.title;
});
if (data.labels.length) {
labelTitles = data.labels.map(function(label) {
return label.title;
});
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
if (labelTitles.length > 5) {
labelTitles = labelTitles.slice(0, 5);
labelTitles.push('and ' + (data.labels.length - 5) + ' more');
}
labelTooltipTitle = labelTitles.join(', ');
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
labelTooltipTitle = labelTitles.join(', ');
}
else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
.tooltip('fixTitle');
$sidebarLabelTooltip
.attr('title', labelTooltipTitle)
.tooltip('fixTitle');
$('.has-tooltip', $value).tooltip({
container: 'body'
});
});
$('.has-tooltip', $value).tooltip({
container: 'body'
});
})
.catch(() => flash(__('Error saving label update.')));
};
$dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: labelUrl
}).done(function(data) {
data = _.chain(data).groupBy(function(label) {
return label.title;
}).map(function(label) {
var color;
color = _.map(label, function(dup) {
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'
axios.get(labelUrl)
.then((res) => {
let data = _.chain(res.data).groupBy(function(label) {
return label.title;
}).map(function(label) {
var color;
color = _.map(label, function(dup) {
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'
});
}
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);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
});
callback(data);
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
})
.catch(() => flash(__('Error fetching labels.')));
},
renderRow: function(label, instance) {
var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
......
import axios from './axios_utils';
import Cache from './cache';
class AjaxCache extends Cache {
......@@ -18,25 +19,18 @@ class AjaxCache extends Cache {
let pendingRequest = this.pendingRequests[endpoint];
if (!pendingRequest) {
pendingRequest = new Promise((resolve, reject) => {
// jQuery 2 is not Promises/A+ compatible (missing catch)
$.ajax(endpoint) // eslint-disable-line promise/catch-or-return
.then(data => resolve(data),
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${endpoint}: ${errorThrown}`);
error.textStatus = textStatus;
reject(error);
},
);
})
.then((data) => {
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((error) => {
delete this.pendingRequests[endpoint];
throw error;
});
pendingRequest = axios.get(endpoint)
.then(({ data }) => {
this.internalStorage[endpoint] = data;
delete this.pendingRequests[endpoint];
})
.catch((e) => {
const error = new Error(`${endpoint}: ${e.message}`);
error.textStatus = e.message;
delete this.pendingRequests[endpoint];
throw error;
});
this.pendingRequests[endpoint] = pendingRequest;
}
......
......@@ -19,6 +19,10 @@ axios.interceptors.response.use((config) => {
window.activeVueResources -= 1;
return config;
}, (e) => {
window.activeVueResources -= 1;
return Promise.reject(e);
});
export default axios;
......
import { getLocationHash } from './url_utility';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => $('body').attr('data-page').split(':')[index];
......@@ -28,16 +28,11 @@ export const isInIssuePage = () => {
return page === 'issues' && action === 'show';
};
export const ajaxGet = url => $.ajax({
type: 'GET',
url,
dataType: 'script',
});
export const ajaxPost = (url, data) => $.ajax({
type: 'POST',
url,
data,
export const ajaxGet = url => axios.get(url, {
params: { format: 'js' },
responseType: 'text',
}).then(({ data }) => {
$.globalEval(data);
});
export const rstrip = (val) => {
......@@ -412,7 +407,6 @@ window.gl.utils = {
getGroupSlug,
isInIssuePage,
ajaxGet,
ajaxPost,
rstrip,
updateTooltipTitle,
disableButtonIfEmptyField,
......
/* eslint-disable no-param-reassign, comma-dangle */
import axios from '../lib/utils/axios_utils';
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
......@@ -10,20 +11,11 @@
}
fetchConflictsData() {
return $.ajax({
dataType: 'json',
url: this.conflictsPath
});
return axios.get(this.conflictsPath);
}
submitResolveConflicts(data) {
return $.ajax({
url: this.resolveConflictsPath,
data: JSON.stringify(data),
contentType: 'application/json',
dataType: 'json',
method: 'POST'
});
return axios.post(this.resolveConflictsPath, data);
}
}
......
......@@ -38,24 +38,23 @@ $(() => {
showDiffViewTypeSwitcher() { return mergeConflictsStore.fileTextTypePresent(); }
},
created() {
mergeConflictsService
.fetchConflictsData()
.done((data) => {
mergeConflictsService.fetchConflictsData()
.then(({ data }) => {
if (data.type === 'error') {
mergeConflictsStore.setFailedRequest(data.message);
} else {
mergeConflictsStore.setConflictsData(data);
}
})
.error(() => {
mergeConflictsStore.setFailedRequest();
})
.always(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
syntaxHighlight($('.js-syntax-highlight'));
});
})
.catch(() => {
mergeConflictsStore.setLoadingState(false);
mergeConflictsStore.setFailedRequest();
});
},
methods: {
......@@ -82,10 +81,10 @@ $(() => {
mergeConflictsService
.submitResolveConflicts(mergeConflictsStore.getCommitData())
.done((data) => {
.then(({ data }) => {
window.location.href = data.redirect_to;
})
.error(() => {
.catch(() => {
mergeConflictsStore.setSubmitState(false);
new Flash('Failed to save merge conflicts resolutions. Please try again!');
});
......
/* eslint-disable no-new, class-methods-use-this */
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 initChangesDropdown from './init_changes_dropdown';
import bp from './breakpoints';
......@@ -244,15 +245,22 @@ export default class MergeRequestTabs {
if (this.commitsLoaded) {
return;
}
this.ajaxGet({
url: `${source}.json`,
success: (data) => {
this.toggleLoading(true);
axios.get(`${source}.json`)
.then(({ data }) => {
document.querySelector('div#commits').innerHTML = data.html;
localTimeAgo($('.js-timeago', 'div#commits'));
this.commitsLoaded = true;
this.scrollToElement('#commits');
},
});
this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
}
mountPipelinesView() {
......@@ -283,9 +291,10 @@ export default class MergeRequestTabs {
// some pages like MergeRequestsController#new has query parameters on that anchor
const urlPathname = parseUrlPathname(source);
this.ajaxGet({
url: `${urlPathname}.json${location.search}`,
success: (data) => {
this.toggleLoading(true);
axios.get(`${urlPathname}.json${location.search}`)
.then(({ data }) => {
const $container = $('#diffs');
$container.html(data.html);
......@@ -335,8 +344,13 @@ export default class MergeRequestTabs {
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
}
},
});
this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
flash('An error occurred while fetching this tab.');
});
}
// Show or hide the loading spinner
......@@ -346,17 +360,6 @@ export default class MergeRequestTabs {
$('.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() {
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 {
constructor() {
......@@ -33,15 +34,12 @@ export default class Milestone {
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
});
axios.get(endpoint)
.then(({ data }) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
})
.catch(() => flash('Error loading milestone tab'));
}
}
}
......@@ -2,6 +2,7 @@
/* global Issuable */
/* global ListMilestone */
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import { timeFor } from './lib/utils/datetime_utility';
export default class MilestoneSelect {
......@@ -52,48 +53,47 @@ export default class MilestoneSelect {
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
data: (term, callback) => $.ajax({
url: milestonesUrl
}).done((data) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
data: (term, callback) => axios.get(milestonesUrl)
.then(({ data }) => {
const extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
name: '',
title: 'Any Milestone'
});
}
if (showNo) {
extraOptions.push({
id: -1,
name: 'No Milestone',
title: 'No Milestone'
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
title: 'Upcoming'
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
title: 'Started'
});
}
if (extraOptions.length) {
extraOptions.push('divider');
}
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}),
callback(extraOptions.concat(data));
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<a href='#' class='dropdown-menu-milestone-link'>
......@@ -200,26 +200,23 @@ export default class MilestoneSelect {
data[abilityName].milestone_id = selected != null ? selected : null;
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
url: issueUpdateURL,
data: data
}).done((data) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
return axios.put(issueUpdateURL, data)
.then(({ data }) => {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectBox.hide();
$value.css('display', '');
if (data.milestone != null) {
data.milestone.full_path = this.currentProject.full_path;
data.milestone.remaining = timeFor(data.milestone.due_date);
data.milestone.name = data.milestone.title;
$value.html(milestoneLinkTemplate(data.milestone));
return $sidebarCollapsedValue.find('span').html(collapsedSidebarLabelTemplate(data.milestone));
} else {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
});
}
}
});
......
......@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
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 { localTimeAgo } from './lib/utils/datetime_utility';
......@@ -1399,7 +1399,7 @@ export default class Notes {
* 2) Identify comment type; a) Main thread b) Discussion thread c) Discussion resolve
* 3) Build temporary placeholder element (using `createPlaceholderNote`)
* 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
* 1. Remove placeholder element
* 2. Show submitted Note element
......@@ -1481,8 +1481,10 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
ajaxPost(formAction, formData)
.then((note) => {
axios.post(formAction, formData)
.then((res) => {
const note = res.data;
// Submission successful! remove placeholder
$notesContainer.find(`#${noteUniqueId}`).remove();
......@@ -1555,7 +1557,7 @@ export default class Notes {
}
$form.trigger('ajax:success', [note]);
}).fail(() => {
}).catch(() => {
// Submission failed, remove placeholder note and show Flash error message
$notesContainer.find(`#${noteUniqueId}`).remove();
......@@ -1594,7 +1596,7 @@ export default class Notes {
*
* 1) Get Form metadata
* 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
* 1. Show submitted Note element
* b) If request failed
......@@ -1625,12 +1627,12 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
ajaxPost(formAction, formData)
.then((note) => {
axios.post(formAction, formData)
.then(({ data }) => {
// Submission successful! render final note element
this.updateNote(note, $editingNote);
this.updateNote(data, $editingNote);
})
.fail(() => {
.catch(() => {
// Submission failed, revert back to original note
$noteBodyText.html(_.escape(cachedNoteBodyText));
$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 */
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';
export default class Project {
......@@ -79,17 +82,15 @@ export default class Project {
$dropdown = $(this);
selected = $dropdown.data('selected');
return $dropdown.glDropdown({
data: function(term, callback) {
return $.ajax({
url: $dropdown.data('refs-url'),
data: {
data(term, callback) {
axios.get($dropdown.data('refs-url'), {
params: {
ref: $dropdown.data('ref'),
search: term,
},
dataType: 'json',
}).done(function(refs) {
return callback(refs);
});
})
.then(({ data }) => callback(data))
.catch(() => flash(__('An error occurred while getting projects')));
},
selectable: true,
filterable: true,
......
......@@ -48,7 +48,7 @@
};
</script>
<template>
<ul class="nav-links scrolling-tabs">
<ul class="nav-links scrolling-tabs separator">
<li
v-for="(tab, i) in tabs"
:key="i"
......
......@@ -82,6 +82,10 @@
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-xs-max) {
width: 100%;
&.mobile-separator {
border-bottom: 1px solid $border-color;
}
}
}
......@@ -168,9 +172,9 @@
display: inline-block;
}
// Applies on /dashboard/issues
.project-item-select-holder {
margin: 0;
width: 100%;
}
&.inline {
......@@ -367,7 +371,6 @@
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
float: right;
......
class Admin::ServicesController < Admin::ApplicationController
include ServiceParams
before_action :whitelist_query_limiting, only: [:index]
before_action :service, only: [:edit, :update]
def index
......@@ -37,4 +38,8 @@ class Admin::ServicesController < Admin::ApplicationController
def service
@service ||= Service.where(id: params[:id], template: true).first
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42430')
end
end
......@@ -4,6 +4,7 @@ module Boards
prepend EE::Boards::IssuesController
include BoardsResponses
before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index]
before_action :authorize_create_issue, only: [:create]
before_action :authorize_update_issue, only: [:update]
......@@ -94,5 +95,10 @@ module Boards
}
)
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
class Import::GitlabProjectsController < Import::BaseController
before_action :whitelist_query_limiting, only: [:create]
before_action :verify_gitlab_project_import_enabled
def new
......@@ -40,4 +41,8 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42437')
end
end
......@@ -4,6 +4,7 @@ class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
include RendersCommits
before_action :whitelist_query_limiting
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
......@@ -65,4 +66,8 @@ class Projects::CommitsController < Projects::ApplicationController
@commits = @commits.with_pipeline_status
@commits = prepare_commits_for_rendering(@commits)
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42330')
end
end
......@@ -3,6 +3,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
include ActionView::Helpers::TextHelper
include CycleAnalyticsParams
before_action :whitelist_query_limiting, only: [:show]
before_action :authorize_read_cycle_analytics!
def show
......@@ -31,4 +32,8 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
permissions: @cycle_analytics.permissions(user: current_user)
}
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42671')
end
end
......@@ -2,6 +2,7 @@ class Projects::ForksController < Projects::ApplicationController
include ContinueParams
# Authorize
before_action :whitelist_query_limiting, only: [:create]
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create]
......@@ -54,4 +55,8 @@ class Projects::ForksController < Projects::ApplicationController
render :error
end
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42335')
end
end
......@@ -8,6 +8,8 @@ class Projects::IssuesController < Projects::ApplicationController
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 :issue, except: [:index, :new, :create, :bulk_update, :export_csv]
......@@ -250,4 +252,17 @@ class Projects::IssuesController < Projects::ApplicationController
@finder_type = IssuesFinder
super
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
......@@ -6,6 +6,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
prepend ::EE::Projects::MergeRequests::CreationsController
skip_before_action :merge_request
before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
......@@ -127,4 +128,8 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
@project.forked_from_project
end
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42384')
end
end
......@@ -9,6 +9,8 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
prepend ::EE::Projects::MergeRequestsController
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 :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
......@@ -348,4 +350,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
access_denied! unless access_check
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
......@@ -2,6 +2,7 @@ class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath
include ApplicationHelper
before_action :whitelist_query_limiting
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
......@@ -35,4 +36,8 @@ class Projects::NetworkController < Projects::ApplicationController
@options[:extended_sha1] = params[:extended_sha1]
@commit = @repo.commit(@options[:extended_sha1])
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42333')
end
end
......@@ -2,6 +2,7 @@ class Projects::NotesController < Projects::ApplicationController
include NotesActions
include ToggleAwardEmoji
before_action :whitelist_query_limiting, only: [:create]
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
......@@ -79,4 +80,8 @@ class Projects::NotesController < Projects::ApplicationController
access_denied! unless can?(current_user, :create_note, noteable)
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42383')
end
end
class Projects::PipelinesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :retry]
before_action :pipeline, except: [:index, :new, :create, :charts]
before_action :commit, only: [:show, :builds, :failures]
before_action :authorize_read_pipeline!
......@@ -166,4 +167,9 @@ class Projects::PipelinesController < Projects::ApplicationController
def commit
@commit ||= @pipeline.commit
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
......@@ -4,6 +4,7 @@ class ProjectsController < Projects::ApplicationController
include PreviewMarkdown
prepend EE::ProjectsController
before_action :whitelist_query_limiting, only: [:create]
before_action :authenticate_user!, except: [:index, :show, :activity, :refs]
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create]
......@@ -415,4 +416,8 @@ class ProjectsController < Projects::ApplicationController
#
redirect_to request.original_url.sub(%r{\.git/?\Z}, '') if params[:format] == 'git'
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440')
end
end
......@@ -2,6 +2,8 @@ class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
prepend EE::RegistrationsController
before_action :whitelist_query_limiting, only: [:destroy]
def new
redirect_to(new_user_session_path)
end
......@@ -84,4 +86,8 @@ class RegistrationsController < Devise::RegistrationsController
def devise_mapping
@devise_mapping ||= Devise.mappings[:user]
end
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42380')
end
end
......@@ -87,20 +87,10 @@ module Storage
remove_exports!
end
def remove_exports!
Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
end
def export_path
File.join(Gitlab::ImportExport.storage_path, full_path_was)
end
def remove_legacy_exports!
legacy_export_path = File.join(Gitlab::ImportExport.storage_path, full_path_was)
def full_path_was
if parent
parent.full_path + '/' + path_was
else
path_was
end
FileUtils.rm_rf(legacy_export_path)
end
end
end
......@@ -312,12 +312,6 @@ class Group < Namespace
list_of_ids.reverse.map { |group| variables[group.id] }.compact.flatten
end
def full_path_was
return path_was unless has_parent?
"#{parent.full_path}/#{path_was}"
end
def group_member(user)
if group_members.loaded?
group_members.find { |gm| gm.user_id == user.id }
......
......@@ -235,6 +235,24 @@ class Namespace < ActiveRecord::Base
feature_available?(:multiple_issue_boards)
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
def refresh_access_of_projects_invited_groups
......
......@@ -73,6 +73,7 @@ class Project < ActiveRecord::Base
before_destroy :remove_private_deploy_keys
after_destroy -> { run_after_commit { remove_pages } }
after_destroy :remove_exports
after_validation :check_pending_delete
......@@ -1537,6 +1538,8 @@ class Project < ActiveRecord::Base
end
def export_path
return nil unless namespace.present? || hashed_storage?(:repository)
File.join(Gitlab::ImportExport.storage_path, disk_path)
end
......@@ -1545,8 +1548,9 @@ class Project < ActiveRecord::Base
end
def remove_exports
_, status = Gitlab::Popen.popen(%W(find #{export_path} -not -path #{export_path} -delete))
status.zero?
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
end
def full_path_slug
......
......@@ -91,6 +91,10 @@ module MergeRequests
merge_request.mark_as_unchecked
UpdateHeadPipelineForMergeRequestWorker.perform_async(merge_request.id)
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
# Note: Closed merge requests also need approvals reset.
......@@ -212,7 +216,8 @@ module MergeRequests
merge_requests.uniq.select(&:source_project)
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)
end
......
class AttachmentUploader < GitlabUploader
include UploaderHelper
include RecordsUploads::Concern
include ObjectStorage::Concern
prepend ObjectStorage::Extension::RecordsUploads
include UploaderHelper
private
......
......@@ -8,11 +8,11 @@ class AvatarUploader < GitlabUploader
model.avatar.file && model.avatar.file.present?
end
def move_to_store
def move_to_cache
false
end
def move_to_cache
def move_to_store
false
end
......
......@@ -15,8 +15,6 @@ class FileUploader < GitlabUploader
MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
DYNAMIC_PATH_PATTERN = %r{(?<secret>\h{32})/(?<identifier>.*)}
attr_accessor :model
def self.root
File.join(options.storage_path, 'uploads')
end
......@@ -62,6 +60,8 @@ class FileUploader < GitlabUploader
SecureRandom.hex
end
attr_accessor :model
def initialize(model, secret = nil)
@model = model
@secret = secret
......
......@@ -45,6 +45,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
file.present?
end
def store_dir
File.join(base_dir, dynamic_segment)
end
def cache_dir
File.join(root, base_dir, 'tmp/cache')
end
......@@ -62,10 +66,6 @@ class GitlabUploader < CarrierWave::Uploader::Base
# 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
# 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
raise(NotImplementedError)
end
......
.top-area
%ul.nav-links
%ul.nav-links.mobile-separator
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: _("Your groups") do
Your groups
......
......@@ -4,7 +4,7 @@
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
.fade-left= icon('angle-left')
.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
= link_to dashboard_projects_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your projects
......
.nav-block
%ul.nav-links
%ul.nav-links.mobile-separator
= nav_link(html_options: { class: ("active" unless params[:personal].present?) }) do
= link_to s_('DashboardProjects|All'), dashboard_projects_path
= nav_link(html_options: { class: ("active" if params[:personal].present?) }) do
......
......@@ -4,7 +4,7 @@
- if current_user.todos.any?
.top-area
%ul.nav-links
%ul.nav-links.mobile-separator
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
......
......@@ -15,7 +15,7 @@
.col-sm-7.brand-holder.pull-left
%h1
= brand_title
= brand_image
= brand_image
- if brand_item&.description?
= brand_text
- else
......
%ul.nav-links
%ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }>
= link_to schedule_path_proc.call(nil) do
= s_("PipelineSchedules|All")
......
- failed_builds = @pipeline.statuses.latest.failed
.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
= link_to project_pipeline_path(@project, @pipeline), data: { target: 'div#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
Pipeline
......
%ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- if event_filter_visible(:issues)
= 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')
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)
= event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events')
- if event_filter_visible(:merge_requests)
= event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events')
- if event_filter_visible(:issues)
= 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) }>
= link_to milestones_filter_path(state: 'opened') do
Open
......
%ul.nav-links
%ul.nav-links.mobile-separator
%li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do
All
......
- type = local_assigns.fetch(:type, :issues)
- 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') }>
= 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)}
......
- subject = local_assigns.fetch(:subject, current_user)
- 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?) }
= link_to subject_snippets_path(subject) do
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
# Settings.artifact['path'] is deprecated, use `storage_path` instead
Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings.artifacts['object_store'] ||= Settingslogic.new({})
Settings.artifacts['object_store']['enabled'] ||= false
Settings.artifacts['object_store']['remote_directory'] ||= nil
......@@ -413,6 +412,13 @@ Settings.uploads['object_store']['background_upload'] ||= true
# Convert upload connection settings to use string keys, to make Fog happy
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
#
......
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 @@
#
# 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
enable_extension "plpgsql"
......@@ -1215,7 +1215,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do
t.datetime "last_edited_at"
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
t.datetime "closed_at"
t.datetime_with_timezone "closed_at"
end
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
......@@ -2229,7 +2229,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do
t.integer "project_id", null: false
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|
t.text "certificate"
......
......@@ -75,6 +75,7 @@ comments: false
- [Ordering table columns](ordering_table_columns.md)
- [Verifying database capabilities](verifying_database_capabilities.md)
- [Database Debugging and Troubleshooting](database_debugging.md)
- [Query Count Limits](query_count_limits.md)
## 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 Geo
class JobArtifactUploader < FileUploader
class JobArtifactUploader < ::Gitlab::Geo::FileUploader
def execute
job_artifact = ::Ci::JobArtifact.find_by(id: object_db_id)
......
......@@ -39,10 +39,11 @@ module API
# Helper Methods for Grape Endpoint
module HelperMethods
prepend EE::API::APIGuard::HelperMethods
include Gitlab::Auth::UserAuthFinders
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
forbidden!('User is blocked') unless Gitlab::UserAccess.new(user).allowed? && user.can?(:access_api)
......@@ -50,6 +51,10 @@ module API
user
end
def find_user_from_sources
find_user_from_access_token || find_user_from_warden
end
private
# An array of scopes that were registered (using `allow_access_with_scope`)
......
......@@ -29,6 +29,8 @@ module API
use :pagination
end
get ':id/repository/branches' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42329')
repository = user_project.repository
branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name))
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
......
......@@ -54,6 +54,8 @@ module API
find_params[:parent] = find_group!(params[:id]) if params[:id]
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.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort])
......@@ -181,6 +183,8 @@ module API
desc 'Remove a group.'
delete ":id" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4795')
group = find_group!(params[:id])
authorize! :admin_group, group
......
......@@ -166,6 +166,8 @@ module API
use :issue_params
end
post ':id/issues' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42320')
authorize! :create_issue, user_project
# Setting created_at time only allowed for admins and project owners
......@@ -207,6 +209,8 @@ module API
:weight, :discussion_locked
end
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))
authorize! :update_issue, issue
......@@ -240,6 +244,8 @@ module API
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
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])
not_found!('Issue') unless issue
......
......@@ -164,6 +164,8 @@ module API
use :optional_params
end
post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42316')
authorize! :create_merge_request, user_project
mr_params = declared_params(include_missing: false)
......@@ -273,6 +275,8 @@ module API
at_least_one_of(*(at_least_one_of_ce + at_least_one_of_ee))
end
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)
mr_params = declared_params(include_missing: false)
......@@ -303,6 +307,8 @@ module API
optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
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_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
......
......@@ -42,6 +42,8 @@ module API
requires :ref, type: String, desc: 'Reference'
end
post ':id/pipeline' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42124')
authorize! :create_pipeline, user_project
new_pipeline = Ci::CreatePipelineService.new(user_project,
......
......@@ -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'
end
post ':id/fork' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42284')
fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace]
......
......@@ -15,6 +15,8 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42283')
# validate variables
params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
......
......@@ -389,6 +389,8 @@ module API
optional :hard_delete, type: Boolean, desc: "Whether to remove a user's contributions"
end
delete ":id" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42279')
authenticated_as_admin!
user = User.find_by(id: params[:id])
......
......@@ -14,6 +14,8 @@ module API
success ::API::Entities::Branch
end
get ":id/repository/branches" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42276')
repository = user_project.repository
branches = repository.branches.sort_by(&:name)
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
......
......@@ -151,6 +151,8 @@ module API
desc 'Remove a group.'
delete ":id" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ee/issues/4797')
group = find_group!(params[:id])
authorize! :admin_group, group
present ::Groups::DestroyService.new(group, current_user).execute, with: Entities::GroupDetail, current_user: current_user
......
......@@ -135,6 +135,8 @@ module API
use :issue_params
end
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
unless current_user.admin? || user_project.owner == current_user
params.delete(:created_at)
......@@ -171,6 +173,8 @@ module API
:weight
end
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))
authorize! :update_issue, issue
......@@ -203,6 +207,8 @@ module API
requires :to_project_id, type: Integer, desc: 'The ID of the new project'
end
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])
not_found!('Issue') unless issue
......
......@@ -93,6 +93,8 @@ module API
use :optional_params
end
post ":id/merge_requests" do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42126')
authorize! :create_merge_request, user_project
mr_params = declared_params(include_missing: false)
......@@ -169,6 +171,8 @@ module API
:remove_source_branch, :squash
end
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)
mr_params = declared_params(include_missing: false)
......@@ -196,6 +200,8 @@ module API
optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
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 can not be merged
......
......@@ -19,6 +19,8 @@ module API
desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42123')
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute
......
......@@ -16,6 +16,8 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end
post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42121')
# validate variables
params[:variables] = params[:variables].to_h
unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
......
......@@ -1222,33 +1222,13 @@ module Gitlab
end
def 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)
gitaly_migrate(:squash) do |is_enabled|
if is_enabled
gitaly_operation_client.user_squash(user, squash_id, branch,
start_sha, end_sha, author, message)
else
git_squash(user, squash_id, branch, start_sha, end_sha, author, message)
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
......@@ -2164,6 +2144,37 @@ module Gitlab
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:)
args = %W(fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref})
run_git(args)
......
......@@ -183,6 +183,32 @@ module Gitlab
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(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
......
......@@ -139,13 +139,12 @@ module Gitlab
end
def setup_label
return unless @relation_hash['type'] == 'GroupLabel'
# 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_name = :group_label
else
@relation_hash['group_id'] = nil
@relation_hash['type'] = 'ProjectLabel'
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'
task :flay do
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
exit 1
end
......
......@@ -125,6 +125,12 @@ FactoryBot.define do
avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :with_export do
after(:create) do |project, evaluator|
ProjectExportWorker.new.perform(project.creator.id, project.id)
end
end
trait :broken_storage do
after(:create) do |project|
project.update_column(:repository_storage, 'broken')
......
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') }
let(:project) { create(:project) }
background do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
before do
allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
context 'admin user' do
shared_examples_for 'handling project exports on namespace change' do
let!(:old_export_path) { project.export_path }
before do
sign_in(create(:admin))
setup_export_project
end
context 'moving the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
it 'removes the export file' do
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)
end
end
context 'deleting the namespace' do
scenario 'removes the export file' do
setup_export_project
old_export_path = project.export_path.dup
it 'removes the export file' do
expect(File).to exist(old_export_path)
project.namespace.destroy
......@@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(File).not_to exist(old_export_path)
end
end
end
def setup_export_project
visit edit_project_path(project)
describe 'legacy storage' do
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')
end
expect(page).to have_content('Export project')
find(:link, 'Export project').send_keys(:return)
visit edit_project_path(project)
expect(page).to have_content('Download export')
end
end
/* global BoardService */
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import '~/labels_select';
import LabelsSelect from '~/boards/components/labels_select.vue';
import IssuableContext from '~/issuable_context';
......@@ -32,14 +34,16 @@ const label2 = {
};
describe('LabelsSelect', () => {
let mock;
beforeEach((done) => {
setFixtures('<div class="test-container"></div>');
const deferred = new jQuery.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.resolve([
mock = new MockAdapter(axios);
mock.onGet('/some/path').reply(200, [
label,
label2,
]));
]);
// eslint-disable-next-line no-new
new IssuableContext();
......@@ -60,6 +64,10 @@ describe('LabelsSelect', () => {
Vue.nextTick(done);
});
afterEach(() => {
mock.restore();
});
describe('canEdit', () => {
it('hides Edit button', (done) => {
vm.canEdit = false;
......
/* global BoardService */
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 IssuableContext from '~/issuable_context';
import { boardObj } from './mock_data';
......@@ -92,12 +94,18 @@ describe('Milestone select component', () => {
});
describe('clicking dropdown items', () => {
let mock;
beforeEach(() => {
const deferred = new jQuery.Deferred();
spyOn($, 'ajax').and.returnValue(deferred.resolve([
mock = new MockAdapater(axios);
mock.onGet('/test/issue-boards/milestones.json').reply(200, [
milestone,
milestone2,
]));
]);
});
afterEach(() => {
mock.restore();
});
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 */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Issue from '~/issue';
import '~/lib/utils/text_utility';
......@@ -88,20 +90,24 @@ describe('Issue', function() {
[true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
const action = isIssueInitiallyOpen ? 'close' : 'reopen';
let mock;
function ajaxSpy(req) {
if (req.url === this.$triggeredButton.attr('href')) {
expect(req.type).toBe('PUT');
function mockCloseButtonResponseSuccess(url, response) {
mock.onPut(url).reply(() => {
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 null;
return [200, response];
});
}
function mockCloseButtonResponseError(url) {
mock.onPut(url).networkError();
}
function mockCanCreateBranch(canCreateBranch) {
mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
can_create_branch: canCreateBranch,
});
}
beforeEach(function() {
......@@ -111,6 +117,11 @@ describe('Issue', function() {
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);
this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen);
......@@ -120,71 +131,89 @@ describe('Issue', function() {
this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001');
this.issueStateDeferred = new jQuery.Deferred();
this.canCreateBranchDeferred = new jQuery.Deferred();
spyOn(axios, 'get').and.callThrough();
});
spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
afterEach(() => {
mock.restore();
$('div.flash-alert').remove();
});
it(`${action}s the issue`, function() {
this.$triggeredButton.trigger('click');
this.issueStateDeferred.resolve({
it(`${action}s the issue`, function(done) {
mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
id: 34
});
this.canCreateBranchDeferred.resolve({
can_create_branch: !isIssueInitiallyOpen
});
mockCanCreateBranch(!isIssueInitiallyOpen);
expectIssueState(!isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
expectNewBranchButtonState(false, !isIssueInitiallyOpen);
this.$triggeredButton.trigger('click');
setTimeout(() => {
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() {
this.$triggeredButton.trigger('click');
this.issueStateDeferred.resolve({
it(`fails to ${action} the issue if saved:false`, function(done) {
mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
saved: false
});
this.canCreateBranchDeferred.resolve({
can_create_branch: isIssueInitiallyOpen
});
mockCanCreateBranch(isIssueInitiallyOpen);
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
this.$triggeredButton.trigger('click');
setTimeout(() => {
expectIssueState(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.issueStateDeferred.reject();
this.canCreateBranchDeferred.resolve({
can_create_branch: isIssueInitiallyOpen
});
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
setTimeout(() => {
expectIssueState(isIssueInitiallyOpen);
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
expectErrorMessage();
expect(this.$projectIssuesCounter.text()).toBe('1,001');
expectNewBranchButtonState(false, isIssueInitiallyOpen);
done();
});
});
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.issueStateDeferred.reject();
this.canCreateBranchDeferred.reject();
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 = $();
this.canCreateBranchDeferred = null;
this.$triggeredButton.trigger('click');
this.issueStateDeferred.reject();
setTimeout(() => {
expect(axios.get).not.toHaveBeenCalled();
done();
});
});
});
});
......
This diff is collapsed.
/* eslint-disable no-new */
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
......@@ -10,35 +12,44 @@ import '~/users_select';
(() => {
let saveLabelCount = 0;
let mock;
describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => {
loadFixtures('static/issue_sidebar_label.html.raw');
mock = new MockAdapter(axios);
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
spyOn(jQuery, 'ajax').and.callFake((req) => {
const d = $.Deferred();
let LABELS_DATA = [];
mock.onGet('/root/test/labels.json').reply(() => {
const labels = Array(10).fill().map((_, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
}));
if (req.url === '/root/test/labels.json') {
for (let i = 0; i < 10; i += 1) {
LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
} else if (req.url === '/root/test/issues/2.json') {
const tmp = [];
for (let i = 0; i < saveLabelCount; i += 1) {
tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
}
LABELS_DATA = { labels: tmp };
}
return [200, labels];
});
mock.onPut('/root/test/issues/2.json').reply(() => {
const labels = Array(saveLabelCount).fill().map((_, i) => ({
id: i,
title: `test ${i}`,
color: '#5CB85C',
}));
d.resolve(LABELS_DATA);
return d.promise();
return [200, { labels }];
});
});
afterEach(() => {
mock.restore();
});
it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5;
$('.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