diff options
295 files changed, 3964 insertions, 1709 deletions
diff --git a/.flayignore b/.flayignore index acac0ce14c9..87cb3507b05 100644 --- a/.flayignore +++ b/.flayignore @@ -6,3 +6,5 @@ app/models/concerns/relative_positioning.rb app/workers/stuck_merge_jobs_worker.rb lib/gitlab/redis/*.rb lib/gitlab/gitaly_client/operation_service.rb +lib/gitlab/background_migration/* +app/models/project_services/kubernetes_service.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b4afa953175..9c3556f5cce 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -627,6 +627,8 @@ codequality: sast: <<: *except-docs image: registry.gitlab.com/gitlab-org/gl-sast:latest + variables: + CONFIDENCE_LEVEL: 2 before_script: [] script: - /app/bin/run . diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b366ae6f069..ed56da0353d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,6 +50,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ## Contribute to GitLab +For a first-time step-by-step guide to the contribution process, see +["Contributing to GitLab"](https://about.gitlab.com/contributing/). + Thank you for your interest in contributing to GitLab. This guide details how to contribute to GitLab in a way that is efficient for everyone. diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 4a7076db09a..bd14e8533ef 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -0.77.0 +0.78.0 @@ -349,7 +349,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 diff --git a/Gemfile.lock b/Gemfile.lock index 2ddf8221a06..cf9f160499d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -211,7 +211,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) @@ -588,7 +588,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) @@ -1037,7 +1037,7 @@ DEPENDENCIES faraday (~> 0.12) 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) diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js index 7f70fce913a..0d6e0dbefcc 100644 --- a/app/assets/javascripts/behaviors/secret_values.js +++ b/app/assets/javascripts/behaviors/secret_values.js @@ -15,10 +15,12 @@ export default class SecretValues { init() { this.revealButton = this.container.querySelector('.js-secret-value-reveal-button'); - const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); - this.updateDom(isRevealed); + if (this.revealButton) { + const isRevealed = convertPermissionToBoolean(this.revealButton.dataset.secretRevealStatus); + this.updateDom(isRevealed); - this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this)); + } } onRevealButtonClicked() { diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index 583e5faa506..37074301b51 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -235,7 +235,7 @@ export default class FileTemplateMediator { } setFilename(name) { - this.$filenameInput.val(name); + this.$filenameInput.val(name).trigger('change'); } getSelected() { diff --git a/app/assets/javascripts/ci_variable_list/ci_variable_list.js b/app/assets/javascripts/ci_variable_list/ci_variable_list.js new file mode 100644 index 00000000000..e46478ddb98 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/ci_variable_list.js @@ -0,0 +1,205 @@ +import $ from 'jquery'; +import { convertPermissionToBoolean } from '../lib/utils/common_utils'; +import { s__ } from '../locale'; +import setupToggleButtons from '../toggle_buttons'; +import CreateItemDropdown from '../create_item_dropdown'; +import SecretValues from '../behaviors/secret_values'; + +const ALL_ENVIRONMENTS_STRING = s__('CiVariable|All environments'); + +function createEnvironmentItem(value) { + return { + title: value === '*' ? ALL_ENVIRONMENTS_STRING : value, + id: value, + text: value, + }; +} + +export default class VariableList { + constructor({ + container, + formField, + }) { + this.$container = $(container); + this.formField = formField; + this.environmentDropdownMap = new WeakMap(); + + this.inputMap = { + id: { + selector: '.js-ci-variable-input-id', + default: '', + }, + key: { + selector: '.js-ci-variable-input-key', + default: '', + }, + value: { + selector: '.js-ci-variable-input-value', + default: '', + }, + protected: { + selector: '.js-ci-variable-input-protected', + default: 'true', + }, + environment: { + // We can't use a `.js-` class here because + // gl_dropdown replaces the <input> and doesn't copy over the class + // See https://gitlab.com/gitlab-org/gitlab-ce/issues/42458 + selector: `input[name="${this.formField}[variables_attributes][][environment]"]`, + default: '*', + }, + _destroy: { + selector: '.js-ci-variable-input-destroy', + default: '', + }, + }; + + this.secretValues = new SecretValues({ + container: this.$container[0], + valueSelector: '.js-row:not(:last-child) .js-secret-value', + placeholderSelector: '.js-row:not(:last-child) .js-secret-value-placeholder', + }); + } + + init() { + this.bindEvents(); + this.secretValues.init(); + } + + bindEvents() { + this.$container.find('.js-row').each((index, rowEl) => { + this.initRow(rowEl); + }); + + this.$container.on('click', '.js-row-remove-button', (e) => { + e.preventDefault(); + this.removeRow($(e.currentTarget).closest('.js-row')); + }); + + const inputSelector = Object.keys(this.inputMap) + .map(name => this.inputMap[name].selector) + .join(','); + + // Remove any empty rows except the last row + this.$container.on('blur', inputSelector, (e) => { + const $row = $(e.currentTarget).closest('.js-row'); + + if ($row.is(':not(:last-child)') && !this.checkIfRowTouched($row)) { + this.removeRow($row); + } + }); + + // Always make sure there is an empty last row + this.$container.on('input trigger-change', inputSelector, () => { + const $lastRow = this.$container.find('.js-row').last(); + + if (this.checkIfRowTouched($lastRow)) { + this.insertRow($lastRow); + } + }); + } + + initRow(rowEl) { + const $row = $(rowEl); + + setupToggleButtons($row[0]); + + const $environmentSelect = $row.find('.js-variable-environment-toggle'); + if ($environmentSelect.length) { + const createItemDropdown = new CreateItemDropdown({ + $dropdown: $environmentSelect, + defaultToggleLabel: ALL_ENVIRONMENTS_STRING, + fieldName: `${this.formField}[variables_attributes][][environment]`, + getData: (term, callback) => callback(this.getEnvironmentValues()), + createNewItemFromValue: createEnvironmentItem, + onSelect: () => { + // Refresh the other dropdowns in the variable list + // so they have the new value we just picked + this.refreshDropdownData(); + + $row.find(this.inputMap.environment.selector).trigger('trigger-change'); + }, + }); + + // Clear out any data that might have been left-over from the row clone + createItemDropdown.clearDropdown(); + + this.environmentDropdownMap.set($row[0], createItemDropdown); + } + } + + insertRow($row) { + const $rowClone = $row.clone(); + $rowClone.removeAttr('data-is-persisted'); + + // Reset the inputs to their defaults + Object.keys(this.inputMap).forEach((name) => { + const entry = this.inputMap[name]; + $rowClone.find(entry.selector).val(entry.default); + }); + + this.initRow($rowClone); + + $row.after($rowClone); + } + + removeRow($row) { + const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); + + if (isPersisted) { + $row.hide(); + $row + // eslint-disable-next-line no-underscore-dangle + .find(this.inputMap._destroy.selector) + .val(true); + } else { + $row.remove(); + } + } + + checkIfRowTouched($row) { + return Object.keys(this.inputMap).some((name) => { + const entry = this.inputMap[name]; + const $el = $row.find(entry.selector); + return $el.length && $el.val() !== entry.default; + }); + } + + getAllData() { + // Ignore the last empty row because we don't want to try persist + // a blank variable and run into validation problems. + const validRows = this.$container.find('.js-row').toArray().slice(0, -1); + + return validRows.map((rowEl) => { + const resultant = {}; + Object.keys(this.inputMap).forEach((name) => { + const entry = this.inputMap[name]; + const $input = $(rowEl).find(entry.selector); + if ($input.length) { + resultant[name] = $input.val(); + } + }); + + return resultant; + }); + } + + getEnvironmentValues() { + const valueMap = this.$container.find(this.inputMap.environment.selector).toArray() + .reduce((prevValueMap, envInput) => ({ + ...prevValueMap, + [envInput.value]: envInput.value, + }), {}); + + return Object.keys(valueMap).map(createEnvironmentItem); + } + + refreshDropdownData() { + this.$container.find('.js-row').each((index, rowEl) => { + const environmentDropdown = this.environmentDropdownMap.get(rowEl); + if (environmentDropdown) { + environmentDropdown.refreshData(); + } + }); + } +} diff --git a/app/assets/javascripts/ci_variable_list/native_form_variable_list.js b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js new file mode 100644 index 00000000000..d54ea7df1c3 --- /dev/null +++ b/app/assets/javascripts/ci_variable_list/native_form_variable_list.js @@ -0,0 +1,26 @@ +import VariableList from './ci_variable_list'; + +// Used for the variable list on scheduled pipeline edit page +export default function setupNativeFormVariableList({ + container, + formField = 'variables', +}) { + const $container = $(container); + + const variableList = new VariableList({ + container: $container, + formField, + }); + variableList.init(); + + // Clear out the names in the empty last row so it + // doesn't get submitted and throw validation errors + $container.closest('form').on('submit trigger-submit', () => { + const $lastRow = $container.find('.js-row').last(); + + const isTouched = variableList.checkIfRowTouched($lastRow); + if (!isTouched) { + $lastRow.find('input, textarea').attr('name', ''); + } + }); +} diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index ff9e4485916..46232726510 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -8,6 +8,8 @@ import 'core-js/fn/promise'; import 'core-js/fn/string/code-point-at'; import 'core-js/fn/string/from-code-point'; import 'core-js/fn/symbol'; +import 'core-js/es6/map'; +import 'core-js/es6/weak-map'; // Browser polyfills import 'classlist-polyfill'; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 262ed3783fb..ab28b7d8d44 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -12,9 +12,9 @@ import ShortcutsIssuable from './shortcuts_issuable'; import Diff from './diff'; import SearchAutocomplete from './search_autocomplete'; -(function() { - var Dispatcher; +var Dispatcher; +(function() { Dispatcher = (function() { function Dispatcher() { this.initSearch(); @@ -49,46 +49,16 @@ import SearchAutocomplete from './search_autocomplete'; }); switch (page) { - case 'sessions:new': - import('./pages/sessions/new') - .then(callDefault) - .catch(fail); - break; - case 'projects:boards:show': - case 'projects:boards:index': - import('./pages/projects/boards/index') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; case 'projects:environments:metrics': import('./pages/projects/environments/metrics') .then(callDefault) .catch(fail); break; case 'projects:merge_requests:index': - import('./pages/projects/merge_requests/index') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; case 'projects:issues:index': - import('./pages/projects/issues/index') - .then(callDefault) - .catch(fail); - shortcut_handler = true; - break; case 'projects:issues:show': - import('./pages/projects/issues/show') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; - case 'dashboard:milestones:index': - import('./pages/dashboard/milestones/index') - .then(callDefault) - .catch(fail); - break; case 'projects:milestones:index': import('./pages/projects/milestones/index') .then(callDefault) @@ -318,9 +288,6 @@ import SearchAutocomplete from './search_autocomplete'; shortcut_handler = true; break; case 'projects:show': - import('./pages/projects/show') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; case 'projects:edit': @@ -352,9 +319,6 @@ import SearchAutocomplete from './search_autocomplete'; .catch(fail); break; case 'groups:show': - import('./pages/groups/show') - .then(callDefault) - .catch(fail); shortcut_handler = true; break; case 'groups:group_members:index': @@ -363,7 +327,7 @@ import SearchAutocomplete from './search_autocomplete'; .catch(fail); break; case 'projects:project_members:index': - import('./pages/projects/project_members/') + import('./pages/projects/project_members') .then(callDefault) .catch(fail); break; @@ -605,7 +569,7 @@ import SearchAutocomplete from './search_autocomplete'; } break; case 'profiles': - import('./pages/profiles/index/') + import('./pages/profiles/index') .then(callDefault) .catch(fail); break; @@ -662,8 +626,8 @@ import SearchAutocomplete from './search_autocomplete'; return Dispatcher; })(); +})(); - $(window).on('load', function() { - new Dispatcher(); - }); -}).call(window); +export default function initDispatcher() { + return new Dispatcher(); +} diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index df20e1e9c88..57a1fa107e5 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -461,7 +461,7 @@ class GfmAutoComplete { const accentAChar = decodeURI('%C3%80'); const accentYChar = decodeURI('%C3%BF'); - const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); + const regexp = new RegExp(`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-]|[^\\x00-\\x7a])*)$`, 'gi'); return regexp.exec(targetSubtext); } diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 411c820cc43..ff65ea99e9a 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,7 +1,8 @@ /* 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')); } } diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index 9b5092c5e3f..d0b7ea75082 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -1,4 +1,5 @@ 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(() => { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 664e793fc8e..5ecf81ad11d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -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; diff --git a/app/assets/javascripts/lib/utils/ajax_cache.js b/app/assets/javascripts/lib/utils/ajax_cache.js index 629d8f44e18..616d8952ada 100644 --- a/app/assets/javascripts/lib/utils/ajax_cache.js +++ b/app/assets/javascripts/lib/utils/ajax_cache.js @@ -1,3 +1,4 @@ +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; } diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js index 585214049c7..792871e2ecf 100644 --- a/app/assets/javascripts/lib/utils/axios_utils.js +++ b/app/assets/javascripts/lib/utils/axios_utils.js @@ -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; diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 8018ec411c1..5811d059e0b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -1,5 +1,5 @@ -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, diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index d8b881a8fac..39445a85c77 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -33,7 +33,7 @@ import './projects_dropdown'; import './render_gfm'; import initBreadcrumbs from './breadcrumb'; -import './dispatcher'; +import initDispatcher from './dispatcher'; // eslint-disable-next-line global-require, import/no-commonjs if (process.env.NODE_ENV !== 'production') require('./test_utils/'); @@ -265,4 +265,6 @@ $(() => { removeFlashClickListener(flashEl); }); } + + initDispatcher(); }); diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js index c012b77e0bf..c68b47c9348 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js @@ -1,4 +1,5 @@ /* 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); } } diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js index 792b7523889..b4b3c15108d 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js @@ -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!'); }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index acfc62fe5cb..3e97a8c758d 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -1,7 +1,8 @@ /* 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'); } diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index dd6c6b854bc..b1d74250dfd 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,4 +1,5 @@ -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')); } } } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 0e854295fe3..6581be606eb 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -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'); + } + }); } } }); diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index bcb342f407f..8efb8ac5320 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -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'); diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index 857a6793fe3..885acfac6d0 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -1,4 +1,7 @@ import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; export default function initBroadcastMessagesForm() { $('input#broadcast_message_color').on('input', function onMessageColorInput() { @@ -18,13 +21,15 @@ export default function initBroadcastMessagesForm() { if (message === '') { $('.js-broadcast-message-preview').text('Your message here'); } else { - $.ajax({ - url: previewPath, - type: 'POST', - data: { - broadcast_message: { message }, + axios.post(previewPath, { + broadcast_message: { + message, }, - }); + }) + .then(({ data }) => { + $('.js-broadcast-message-preview').html(data.message); + }) + .catch(() => flash(__('An error occurred while rendering preview broadcast message'))); } }, 250)); } diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js index 0f2f1bd4a25..38ddebe30d9 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/index/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -1,3 +1,3 @@ import projectSelect from '~/project_select'; -export default projectSelect; +document.addEventListener('DOMContentLoaded', projectSelect); diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js index 6ed0f010f15..5c763986da3 100644 --- a/app/assets/javascripts/pages/groups/show/index.js +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -7,7 +7,7 @@ import ProjectsList from '~/projects_list'; import ShortcutsNavigation from '~/shortcuts_navigation'; import initGroupsList from '../../../groups'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); new ShortcutsNavigation(); new NotificationsForm(); @@ -19,4 +19,4 @@ export default () => { } initGroupsList(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 42c9bb5ec99..3aeeedbb45d 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,7 +1,7 @@ import UsersSelect from '~/users_select'; import ShortcutsNavigation from '~/shortcuts_navigation'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index 0d3f35f044d..39c043edc38 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -7,10 +7,10 @@ import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { initFilteredSearch(FILTERED_SEARCH.ISSUES); new IssuableIndex(ISSUABLE_INDEX.ISSUE); new ShortcutsNavigation(); new UsersSelect(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index 48ed8fb2243..da312c1f1b7 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,13 +1,13 @@ - /* eslint-disable no-new */ + import initIssuableSidebar from '~/init_issuable_sidebar'; import Issue from '~/issue'; import ShortcutsIssuable from '~/shortcuts_issuable'; import ZenMode from '~/zen_mode'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Issue(); new ShortcutsIssuable(); new ZenMode(); initIssuableSidebar(); -}; +}); diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index b386e8fb48d..adadbf28e49 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -5,9 +5,9 @@ import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { initFilteredSearch(FILTERED_SEARCH.MERGE_REQUESTS); new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new -}; +}); diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index e30d558726b..863dac0d20e 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -1,7 +1,10 @@ /* 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 { @@ -67,17 +70,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, diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 55154cdddcb..9b87f249f09 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -8,7 +8,7 @@ import { ajaxGet } from '~/lib/utils/common_utils'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new Star(); // eslint-disable-line no-new notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new @@ -24,4 +24,4 @@ export default () => { $('#tree-slider').waitForImages(() => { ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); }); -}; +}); diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js index f163557babc..a0aa0499776 100644 --- a/app/assets/javascripts/pages/sessions/new/index.js +++ b/app/assets/javascripts/pages/sessions/new/index.js @@ -2,10 +2,10 @@ import UsernameValidator from './username_validator'; import SigninTabsMemoizer from './signin_tabs_memoizer'; import OAuthRememberMe from './oauth_remember_me'; -export default () => { +document.addEventListener('DOMContentLoaded', () => { new UsernameValidator(); // eslint-disable-line no-new new SigninTabsMemoizer(); // eslint-disable-line no-new new OAuthRememberMe({ // eslint-disable-line no-new container: $('.omniauth-container'), }).bindEvents(); -}; +}); diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index bb34d5d2008..745543c22da 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -1,6 +1,9 @@ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline'; @@ -77,12 +80,9 @@ export default class UsernameValidator { this.state.pending = true; this.state.available = false; this.renderState(); - return $.ajax({ - type: 'GET', - url: `${gon.relative_url_root}/users/${username}/exists`, - dataType: 'json', - success: (res) => this.setAvailabilityState(res.exists) - }); + axios.get(`${gon.relative_url_root}/users/${username}/exists`) + .then(({ data }) => this.setAvailabilityState(data.exists)) + .catch(() => flash(__('An error occurred while validating username'))); } } diff --git a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js index f1cf6e92ef5..0b1a81bae13 100644 --- a/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js +++ b/app/assets/javascripts/pipeline_schedules/pipeline_schedule_form_bundle.js @@ -4,7 +4,7 @@ import GlFieldErrors from '../gl_field_errors'; import intervalPatternInput from './components/interval_pattern_input.vue'; import TimezoneDropdown from './components/timezone_dropdown'; import TargetBranchDropdown from './components/target_branch_dropdown'; -import { setupPipelineVariableList } from './setup_pipeline_variable_list'; +import setupNativeFormVariableList from '../ci_variable_list/native_form_variable_list'; Vue.use(Translate); @@ -42,5 +42,8 @@ document.addEventListener('DOMContentLoaded', () => { gl.targetBranchDropdown = new TargetBranchDropdown(); gl.pipelineScheduleFieldErrors = new GlFieldErrors(formElement); - setupPipelineVariableList($('.js-pipeline-variable-list')); + setupNativeFormVariableList({ + container: $('.js-ci-variable-list-section'), + formField: 'schedule', + }); }); diff --git a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js b/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js deleted file mode 100644 index 9e0e5cacb11..00000000000 --- a/app/assets/javascripts/pipeline_schedules/setup_pipeline_variable_list.js +++ /dev/null @@ -1,73 +0,0 @@ -import { convertPermissionToBoolean } from '../lib/utils/common_utils'; - -function insertRow($row) { - const $rowClone = $row.clone(); - $rowClone.removeAttr('data-is-persisted'); - $rowClone.find('input, textarea').val(''); - $row.after($rowClone); -} - -function removeRow($row) { - const isPersisted = convertPermissionToBoolean($row.attr('data-is-persisted')); - - if (isPersisted) { - $row.hide(); - $row - .find('.js-destroy-input') - .val(1); - } else { - $row.remove(); - } -} - -function checkIfRowTouched($row) { - return $row.find('.js-user-input').toArray().some(el => $(el).val().length > 0); -} - -function setupPipelineVariableList(parent = document) { - const $parent = $(parent); - - $parent.on('click', '.js-row-remove-button', (e) => { - const $row = $(e.currentTarget).closest('.js-row'); - removeRow($row); - - e.preventDefault(); - }); - - // Remove any empty rows except the last r - $parent.on('blur', '.js-user-input', (e) => { - const $row = $(e.currentTarget).closest('.js-row'); - - const isTouched = checkIfRowTouched($row); - if ($row.is(':not(:last-child)') && !isTouched) { - removeRow($row); - } - }); - - // Always make sure there is an empty last row - $parent.on('input', '.js-user-input', () => { - const $lastRow = $parent.find('.js-row').last(); - - const isTouched = checkIfRowTouched($lastRow); - if (isTouched) { - insertRow($lastRow); - } - }); - - // Clear out the empty last row so it - // doesn't get submitted and throw validation errors - $parent.closest('form').on('submit', () => { - const $lastRow = $parent.find('.js-row').last(); - - const isTouched = checkIfRowTouched($lastRow); - if (!isTouched) { - $lastRow.find('input, textarea').attr('name', ''); - } - }); -} - -export { - setupPipelineVariableList, - insertRow, - removeRow, -}; diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js index 86c7b56198d..464bfb351e7 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/preview_markdown.js @@ -7,6 +7,10 @@ // more than `x` users are referenced. // +import axios from '~/lib/utils/axios_utils'; +import flash from '~/flash'; +import { __ } from '~/locale'; + var lastTextareaPreviewed; var lastTextareaHeight = null; var markdownPreview; @@ -62,21 +66,17 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { success(this.ajaxCache.response); return; } - $.ajax({ - type: 'POST', - url: url, - data: { - text: text - }, - dataType: 'json', - success: (function (response) { - this.ajaxCache = { - text: text, - response: response - }; - success(response); - }).bind(this) - }); + axios.post(url, { + text, + }) + .then(({ data }) => { + this.ajaxCache = { + text: text, + response: data, + }; + success(data); + }) + .catch(() => flash(__('An error occurred while fetching markdown preview'))); }; MarkdownPreview.prototype.hideReferencedUsers = function ($form) { diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js index 974dc3ee052..2d680d0f0dc 100644 --- a/app/assets/javascripts/toggle_buttons.js +++ b/app/assets/javascripts/toggle_buttons.js @@ -13,7 +13,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils'; ``` */ -function updatetoggle(toggle, isOn) { +function updateToggle(toggle, isOn) { toggle.classList.toggle('is-checked', isOn); } @@ -21,7 +21,7 @@ function onToggleClicked(toggle, input, clickCallback) { const previousIsOn = convertPermissionToBoolean(input.value); // Visually change the toggle and start loading - updatetoggle(toggle, !previousIsOn); + updateToggle(toggle, !previousIsOn); toggle.setAttribute('disabled', true); toggle.classList.toggle('is-loading', true); @@ -32,7 +32,7 @@ function onToggleClicked(toggle, input, clickCallback) { }) .catch(() => { // Revert the visuals if something goes wrong - updatetoggle(toggle, previousIsOn); + updateToggle(toggle, previousIsOn); }) .then(() => { // Remove the loading indicator in any case @@ -54,7 +54,7 @@ export default function setupToggleButtons(container, clickCallback = () => {}) const isOn = convertPermissionToBoolean(input.value); // Get the visible toggle in sync with the hidden input - updatetoggle(toggle, isOn); + updateToggle(toggle, isOn); toggle.addEventListener('click', onToggleClicked.bind(null, toggle, input, clickCallback)); }); diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue index 7ac9eadcde0..cb6e9858736 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue @@ -1,11 +1,26 @@ <script> + import tooltip from '../../vue_shared/directives/tooltip'; + export default { name: 'MRWidgetAuthor', + directives: { + tooltip, + }, props: { author: { type: Object, required: true, }, + showAuthorName: { + type: Boolean, + required: false, + default: true, + }, + showAuthorTooltip: { + type: Boolean, + required: false, + default: false, + }, }, computed: { authorUrl() { @@ -21,12 +36,17 @@ <a :href="authorUrl" class="author-link inline" + :v-tooltip="showAuthorTooltip" + :title="author.name" > <img :src="avatarUrl" class="avatar avatar-inline s16" /> - <span class="author"> + <span + class="author" + v-if="showAuthorName" + > {{ author.name }} </span> </a> diff --git a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue index cb8e6072a9b..63d8329e495 100644 --- a/app/assets/javascripts/vue_shared/components/navigation_tabs.vue +++ b/app/assets/javascripts/vue_shared/components/navigation_tabs.vue @@ -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" diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index cff47ea76ec..c4aad24e9c1 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -60,3 +60,4 @@ @import "framework/memory_graph"; @import "framework/responsive_tables"; @import "framework/stacked-progress-bar"; +@import "framework/ci_variable_list"; diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index d0b0c69b18f..c4b046a6d68 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -176,6 +176,11 @@ &.btn-remove { @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } + + &.btn-primary, + &.btn-info { + @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); + } } &.btn-gray { diff --git a/app/assets/stylesheets/framework/ci_variable_list.scss b/app/assets/stylesheets/framework/ci_variable_list.scss new file mode 100644 index 00000000000..8f654ab363c --- /dev/null +++ b/app/assets/stylesheets/framework/ci_variable_list.scss @@ -0,0 +1,88 @@ +.ci-variable-list { + margin-left: 0; + margin-bottom: 0; + padding-left: 0; + list-style: none; + clear: both; +} + +.ci-variable-row { + display: flex; + align-items: flex-end; + + &:not(:last-child) { + margin-bottom: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-bottom: 3 * $gl-btn-padding; + } + } + + &:last-child { + .ci-variable-body-item:last-child { + margin-right: $ci-variable-remove-button-width; + + @media (max-width: $screen-xs-max) { + margin-right: 0; + } + } + + .ci-variable-row-remove-button { + display: none; + } + + @media (max-width: $screen-xs-max) { + .ci-variable-row-body { + margin-right: $ci-variable-remove-button-width; + } + } + } +} + +.ci-variable-row-body { + display: flex; + width: 100%; + + @media (max-width: $screen-xs-max) { + display: block; + } +} + +.ci-variable-body-item { + flex: 1; + + &:not(:last-child) { + margin-right: $gl-btn-padding; + + @media (max-width: $screen-xs-max) { + margin-right: 0; + margin-bottom: $gl-btn-padding; + } + } +} + +.ci-variable-protected-item { + flex: 0 1 auto; + display: flex; + align-items: center; +} + +.ci-variable-row-remove-button { + @include transition(color); + flex-shrink: 0; + display: flex; + justify-content: center; + align-items: center; + width: $ci-variable-remove-button-width; + height: $input-height; + padding: 0; + background: transparent; + border: 0; + color: $gl-text-color-secondary; + + &:hover, + &:focus { + outline: none; + color: $gl-text-color; + } +} diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index 5621505996d..e378e84ca1b 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -16,3 +16,31 @@ background-color: $user-mention-bg-hover; } } + +.gfm-color_chip { + display: inline-block; + margin: 0 0 2px 4px; + vertical-align: middle; + border-radius: 3px; + + $chip-size: 0.9em; + $bg-size: $chip-size / 0.9; + $bg-pos: $bg-size / 2; + + width: $chip-size; + height: $chip-size; + background: $white-light; + background-image: linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%), + linear-gradient(135deg, $gray-dark 25%, transparent 0%, transparent 75%, $gray-dark 0%); + background-size: $bg-size $bg-size; + background-position: 0 0, $bg-pos $bg-pos; + + > span { + display: inline-block; + width: 100%; + height: 100%; + margin-bottom: 2px; + border-radius: 3px; + border: 1px solid $black-transparent; + } +} diff --git a/app/assets/stylesheets/framework/secondary-navigation-elements.scss b/app/assets/stylesheets/framework/secondary-navigation-elements.scss index 5f67126bafa..17c31d6b184 100644 --- a/app/assets/stylesheets/framework/secondary-navigation-elements.scss +++ b/app/assets/stylesheets/framework/secondary-navigation-elements.scss @@ -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%; } } } @@ -340,7 +344,6 @@ .project-item-select-holder.btn-group { display: flex; - max-width: 350px; overflow: hidden; float: right; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index f76c6866463..1cc22f5658d 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -668,9 +668,9 @@ $pipeline-dropdown-line-height: 20px; $pipeline-dropdown-status-icon-size: 18px; /* -Pipeline Schedules +CI variable lists */ -$pipeline-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); +$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding}); /* diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/pages/pipeline_schedules.scss index b698a4f9afa..bc7fa8a26d9 100644 --- a/app/assets/stylesheets/pages/pipeline_schedules.scss +++ b/app/assets/stylesheets/pages/pipeline_schedules.scss @@ -78,84 +78,3 @@ margin-right: 3px; } } - -.pipeline-variable-list { - margin-left: 0; - margin-bottom: 0; - padding-left: 0; - list-style: none; - clear: both; -} - -.pipeline-variable-row { - display: flex; - align-items: flex-end; - - &:not(:last-child) { - margin-bottom: $gl-btn-padding; - } - - @media (max-width: $screen-sm-max) { - padding-right: $gl-col-padding; - } - - &:last-child { - .pipeline-variable-row-remove-button { - display: none; - } - - @media (max-width: $screen-sm-max) { - .pipeline-variable-value-input { - margin-right: $pipeline-variable-remove-button-width; - } - } - - @media (max-width: $screen-xs-max) { - .pipeline-variable-row-body { - margin-right: $pipeline-variable-remove-button-width; - } - } - } -} - -.pipeline-variable-row-body { - display: flex; - width: calc(75% - #{$gl-col-padding}); - padding-left: $gl-col-padding; - - @media (max-width: $screen-sm-max) { - width: 100%; - } - - @media (max-width: $screen-xs-max) { - display: block; - } -} - -.pipeline-variable-key-input { - margin-right: $gl-btn-padding; - - @media (max-width: $screen-xs-max) { - margin-bottom: $gl-btn-padding; - } -} - -.pipeline-variable-row-remove-button { - @include transition(color); - flex-shrink: 0; - display: flex; - justify-content: center; - align-items: center; - width: $pipeline-variable-remove-button-width; - height: $input-height; - padding: 0; - background: transparent; - border: 0; - color: $gl-text-color-secondary; - - &:hover, - &:focus { - outline: none; - color: $gl-text-color; - } -} diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index c49b6459452..a9109a1d4d0 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -1,4 +1,6 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController + include BroadcastMessagesHelper + before_action :finder, only: [:edit, :update, :destroy] def index @@ -37,7 +39,8 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController end def preview - @broadcast_message = BroadcastMessage.new(broadcast_message_params) + broadcast_message = BroadcastMessage.new(broadcast_message_params) + render json: { message: render_broadcast_message(broadcast_message) } end protected diff --git a/app/controllers/admin/cohorts_controller.rb b/app/controllers/admin/cohorts_controller.rb index 9b77c554908..10d9d1b5345 100644 --- a/app/controllers/admin/cohorts_controller.rb +++ b/app/controllers/admin/cohorts_controller.rb @@ -1,6 +1,6 @@ class Admin::CohortsController < Admin::ApplicationController def index - if current_application_settings.usage_ping_enabled + if Gitlab::CurrentSettings.usage_ping_enabled cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do CohortsService.new.execute end diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 4c3d336b3af..a7025b62ad7 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -1,6 +1,7 @@ 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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 95ad38d9230..b04bfaf3e49 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,7 +2,6 @@ require 'gon' require 'fogbugz' class ApplicationController < ActionController::Base - include Gitlab::CurrentSettings include Gitlab::GonHelper include GitlabRoutingHelper include PageLayoutHelper @@ -28,7 +27,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception - helper_method :can?, :current_application_settings + helper_method :can? helper_method :import_sources_enabled?, :github_import_enabled?, :gitea_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled? rescue_from Encoding::CompatibilityError do |exception| @@ -120,7 +119,7 @@ class ApplicationController < ActionController::Base end def after_sign_out_path_for(resource) - current_application_settings.after_sign_out_path.presence || new_user_session_path + Gitlab::CurrentSettings.after_sign_out_path.presence || new_user_session_path end def can?(object, action, subject = :global) @@ -268,15 +267,15 @@ class ApplicationController < ActionController::Base end def import_sources_enabled? - !current_application_settings.import_sources.empty? + !Gitlab::CurrentSettings.import_sources.empty? end def github_import_enabled? - current_application_settings.import_sources.include?('github') + Gitlab::CurrentSettings.import_sources.include?('github') end def gitea_import_enabled? - current_application_settings.import_sources.include?('gitea') + Gitlab::CurrentSettings.import_sources.include?('gitea') end def github_import_configured? @@ -284,7 +283,7 @@ class ApplicationController < ActionController::Base end def gitlab_import_enabled? - request.host != 'gitlab.com' && current_application_settings.import_sources.include?('gitlab') + request.host != 'gitlab.com' && Gitlab::CurrentSettings.import_sources.include?('gitlab') end def gitlab_import_configured? @@ -292,7 +291,7 @@ class ApplicationController < ActionController::Base end def bitbucket_import_enabled? - current_application_settings.import_sources.include?('bitbucket') + Gitlab::CurrentSettings.import_sources.include?('bitbucket') end def bitbucket_import_configured? @@ -300,19 +299,19 @@ class ApplicationController < ActionController::Base end def google_code_import_enabled? - current_application_settings.import_sources.include?('google_code') + Gitlab::CurrentSettings.import_sources.include?('google_code') end def fogbugz_import_enabled? - current_application_settings.import_sources.include?('fogbugz') + Gitlab::CurrentSettings.import_sources.include?('fogbugz') end def git_import_enabled? - current_application_settings.import_sources.include?('git') + Gitlab::CurrentSettings.import_sources.include?('git') end def gitlab_project_import_enabled? - current_application_settings.import_sources.include?('gitlab_project') + Gitlab::CurrentSettings.import_sources.include?('gitlab_project') end # U2F (universal 2nd factor) devices need a unique identifier for the application diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index f8049b20b9f..ee23ee0bcc3 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -2,6 +2,7 @@ module Boards class IssuesController < Boards::ApplicationController 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] @@ -92,5 +93,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 diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb index 688e8bd4a37..997af4ab9e9 100644 --- a/app/controllers/concerns/enforces_two_factor_authentication.rb +++ b/app/controllers/concerns/enforces_two_factor_authentication.rb @@ -20,13 +20,13 @@ module EnforcesTwoFactorAuthentication end def two_factor_authentication_required? - current_application_settings.require_two_factor_authentication? || + Gitlab::CurrentSettings.require_two_factor_authentication? || current_user.try(:require_two_factor_authentication_from_group?) end def two_factor_authentication_reason(global: -> {}, group: -> {}) if two_factor_authentication_required? - if current_application_settings.require_two_factor_authentication? + if Gitlab::CurrentSettings.require_two_factor_authentication? global.call else groups = current_user.expanded_groups_requiring_two_factor_authentication.reorder(name: :asc) @@ -36,7 +36,7 @@ module EnforcesTwoFactorAuthentication end def two_factor_grace_period - periods = [current_application_settings.two_factor_grace_period] + periods = [Gitlab::CurrentSettings.two_factor_grace_period] periods << current_user.two_factor_grace_period if current_user.try(:require_two_factor_authentication_from_group?) periods.min end diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb index 0218ac83441..88d1b34bb06 100644 --- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb +++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb @@ -1,8 +1,6 @@ module RequiresWhitelistedMonitoringClient extend ActiveSupport::Concern - include Gitlab::CurrentSettings - included do before_action :validate_ip_whitelisted_or_valid_token! end @@ -26,7 +24,7 @@ module RequiresWhitelistedMonitoringClient token.present? && ActiveSupport::SecurityUtils.variable_size_secure_compare( token, - current_application_settings.health_check_access_token + Gitlab::CurrentSettings.health_check_access_token ) end diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index 567957ba2cb..f22df992fe9 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -1,4 +1,5 @@ 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 diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 04b29aa2384..52430ea771f 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -51,7 +51,7 @@ class InvitesController < ApplicationController return if current_user notice = "To accept this invitation, sign in" - notice << " or create an account" if current_application_settings.allow_signup? + notice << " or create an account" if Gitlab::CurrentSettings.allow_signup? notice << "." store_location_for :user, request.fullpath diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb index 6b1e64ce819..745abf3c0f5 100644 --- a/app/controllers/koding_controller.rb +++ b/app/controllers/koding_controller.rb @@ -10,6 +10,6 @@ class KodingController < ApplicationController private def check_integration! - render_404 unless current_application_settings.koding_enabled? + render_404 unless Gitlab::CurrentSettings.koding_enabled? end end diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index 2443f529c7b..6a21a3f77ad 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,5 +1,4 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController - include Gitlab::CurrentSettings include Gitlab::GonHelper include PageLayoutHelper include OauthApplications @@ -31,7 +30,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController private def verify_user_oauth_applications_enabled - return if current_application_settings.user_oauth_applications? + return if Gitlab::CurrentSettings.user_oauth_applications? redirect_to profile_path end diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index d631d09f1b8..83c9a3f035e 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -145,7 +145,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController label = Gitlab::OAuth::Provider.label_for(oauth['provider']) message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." - if current_application_settings.allow_signup? + if Gitlab::CurrentSettings.allow_signup? message << " Create a GitLab account first, and then connect it to your #{label} account." end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 57761bfbe26..331583c49e6 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,6 +1,4 @@ class PasswordsController < Devise::PasswordsController - include Gitlab::CurrentSettings - skip_before_action :require_no_authentication, only: [:edit, :update] before_action :resource_from_email, only: [:create] @@ -46,7 +44,7 @@ class PasswordsController < Devise::PasswordsController if resource return if resource.allow_password_authentication? else - return if current_application_settings.password_authentication_enabled? + return if Gitlab::CurrentSettings.password_authentication_enabled? end redirect_to after_sending_reset_password_instructions_path_for(resource_name), diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 0a40c67368f..1d910e461b1 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -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 diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 88ac3ad046b..d1b8fd80c4e 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -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 diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index 68978f8fdd1..f43bba18d81 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -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 diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 384f18b316c..515cb08f1fc 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -8,6 +8,7 @@ class Projects::IssuesController < Projects::ApplicationController prepend_before_action :authenticate_user!, only: [:new] + 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] before_action :set_issuables_index, only: [:index] @@ -247,4 +248,13 @@ 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 end diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 0df80fa700f..a5a2d54ba82 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -4,6 +4,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap include RendersCommits 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] @@ -125,4 +126,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 diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 2e8a738b6d9..8af4e379f0a 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -7,6 +7,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo include IssuableCollections skip_before_action :merge_request, only: [:index, :bulk_update] + 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] @@ -339,4 +340,9 @@ 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 end diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index fb68dd771a1..3b10a93e97f 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -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 diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 5940fae8dd0..4f8978c93c3 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -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 diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index e146d0d3cd5..78d109cf33e 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,4 +1,5 @@ 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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 8158934322d..72573e0765d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -3,6 +3,7 @@ class ProjectsController < Projects::ApplicationController include ExtractsPath include PreviewMarkdown + 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] @@ -394,7 +395,7 @@ class ProjectsController < Projects::ApplicationController end def project_export_enabled - render_404 unless current_application_settings.project_export_enabled? + render_404 unless Gitlab::CurrentSettings.project_export_enabled? end def redirect_git_extension @@ -405,4 +406,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 diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index d9142311b6f..1848c806c41 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,6 +1,8 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify + before_action :whitelist_query_limiting, only: [:destroy] + def new redirect_to(new_user_session_path) end @@ -83,4 +85,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 diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 19e38993038..8acefd58e77 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -23,7 +23,7 @@ class RootController < Dashboard::ProjectsController def redirect_unlogged_user if redirect_to_home_page_url? - redirect_to(current_application_settings.home_page_url) + redirect_to(Gitlab::CurrentSettings.home_page_url) else redirect_to(new_user_session_path) end @@ -48,9 +48,9 @@ class RootController < Dashboard::ProjectsController def redirect_to_home_page_url? # If user is not signed-in and tries to access root_path - redirect him to landing page # Don't redirect to the default URL to prevent endless redirections - return false unless current_application_settings.home_page_url.present? + return false unless Gitlab::CurrentSettings.home_page_url.present? - home_page_url = current_application_settings.home_page_url.chomp('/') + home_page_url = Gitlab::CurrentSettings.home_page_url.chomp('/') root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')] root_urls.exclude?(home_page_url) diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb new file mode 100644 index 00000000000..18cde4a7b1a --- /dev/null +++ b/app/controllers/user_callouts_controller.rb @@ -0,0 +1,23 @@ +class UserCalloutsController < ApplicationController + def create + if ensure_callout.persisted? + respond_to do |format| + format.json { head :ok } + end + else + respond_to do |format| + format.json { head :bad_request } + end + end + end + + private + + def ensure_callout + current_user.callouts.find_or_create_by(feature_name: UserCallout.feature_names[feature_name]) + end + + def feature_name + params.require(:feature_name) + end +end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index e91e1d29d64..7548bc30247 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -1,25 +1,23 @@ module ApplicationSettingsHelper extend self - include Gitlab::CurrentSettings - delegate :allow_signup?, :gravatar_enabled?, :password_authentication_enabled_for_web?, :akismet_enabled?, :koding_enabled?, - to: :current_application_settings + to: :'Gitlab::CurrentSettings.current_application_settings' def user_oauth_applications? - current_application_settings.user_oauth_applications + Gitlab::CurrentSettings.user_oauth_applications end def allowed_protocols_present? - current_application_settings.enabled_git_access_protocol.present? + Gitlab::CurrentSettings.enabled_git_access_protocol.present? end def enabled_protocol - case current_application_settings.enabled_git_access_protocol + case Gitlab::CurrentSettings.enabled_git_access_protocol when 'http' gitlab_config.protocol when 'ssh' @@ -57,7 +55,7 @@ module ApplicationSettingsHelper # toggle button effect. def import_sources_checkboxes(help_block_id) Gitlab::ImportSources.options.map do |name, source| - checked = current_application_settings.import_sources.include?(source) + checked = Gitlab::CurrentSettings.import_sources.include?(source) css_class = checked ? 'active' : '' checkbox_name = 'application_setting[import_sources][]' @@ -72,7 +70,7 @@ module ApplicationSettingsHelper def oauth_providers_checkboxes button_based_providers.map do |source| - disabled = current_application_settings.disabled_oauth_sign_in_sources.include?(source.to_s) + disabled = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources.include?(source.to_s) css_class = 'btn' css_class << ' active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 66dc0b1e6f7..f909f664034 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,6 +1,4 @@ module AuthHelper - include Gitlab::CurrentSettings - PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze @@ -41,7 +39,7 @@ module AuthHelper end def enabled_button_based_providers - disabled_providers = current_application_settings.disabled_oauth_sign_in_sources || [] + disabled_providers = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources || [] button_based_providers.map(&:to_s) - disabled_providers end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index f7bdcc6fd9c..6512617a02d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -1,6 +1,4 @@ module ProjectsHelper - include Gitlab::CurrentSettings - def link_to_project(project) link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do title = content_tag(:span, project.name, class: 'project-name') @@ -214,7 +212,7 @@ module ProjectsHelper project.cache_key, controller.controller_name, controller.action_name, - current_application_settings.cache_key, + Gitlab::CurrentSettings.cache_key, 'v2.5' ] @@ -447,10 +445,10 @@ module ProjectsHelper path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}" - return URI.join(current_application_settings.koding_url, path).to_s + return URI.join(Gitlab::CurrentSettings.koding_url, path).to_s end - current_application_settings.koding_url + Gitlab::CurrentSettings.koding_url end def contribution_guide_path(project) @@ -559,7 +557,7 @@ module ProjectsHelper def restricted_levels return [] if current_user.admin? - current_application_settings.restricted_visibility_levels || [] + Gitlab::CurrentSettings.restricted_visibility_levels || [] end def project_permissions_settings(project) diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb new file mode 100644 index 00000000000..6368e248c6e --- /dev/null +++ b/app/helpers/user_callouts_helper.rb @@ -0,0 +1,14 @@ +module UserCalloutsHelper + GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze + + def show_gke_cluster_integration_callout?(project) + can?(current_user, :create_cluster, project) && + !user_dismissed?(GKE_CLUSTER_INTEGRATION) + end + + private + + def user_dismissed?(feature_name) + current_user&.callouts&.find_by(feature_name: feature_name) + end +end diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index 456598b4c28..c20753ece72 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,6 +1,6 @@ module VersionCheckHelper def version_status_badge - if Rails.env.production? && current_application_settings.version_check_enabled + if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled image_url = VersionCheck.new.url image_tag image_url, class: 'js-version-status-badge' end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index c3d5628f241..e395cda03d3 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -151,12 +151,12 @@ module VisibilityLevelHelper def restricted_visibility_levels(show_all = false) return [] if current_user.admin? && !show_all - current_application_settings.restricted_visibility_levels || [] + Gitlab::CurrentSettings.restricted_visibility_levels || [] end delegate :default_project_visibility, :default_group_visibility, - to: :current_application_settings + to: :'Gitlab::CurrentSettings.current_application_settings' def disallowed_visibility_level?(form_model, level) return false unless form_model.respond_to?(:visibility_level_allowed?) diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 77433acb92a..9d071f2d59a 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -5,6 +5,24 @@ module WebpackHelper javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: force_same_domain)) end + def webpack_controller_bundle_tags + bundles = [] + segments = [*controller.controller_path.split('/'), controller.action_name].compact + + until segments.empty? + begin + asset_paths = gitlab_webpack_asset_paths("pages.#{segments.join('.')}", extension: 'js') + bundles.unshift(*asset_paths) + rescue Webpack::Rails::Manifest::EntryPointMissingError + # no bundle exists for this path + end + + segments.pop + end + + javascript_include_tag(*bundles) + end + # override webpack-rails gem helper until changes can make it upstream def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false) return "" unless source.present? diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb index d0ce827a595..fe5f68ba3d5 100644 --- a/app/mailers/abuse_report_mailer.rb +++ b/app/mailers/abuse_report_mailer.rb @@ -1,13 +1,11 @@ class AbuseReportMailer < BaseMailer - include Gitlab::CurrentSettings - def notify(abuse_report_id) return unless deliverable? @abuse_report = AbuseReport.find(abuse_report_id) mail( - to: current_application_settings.admin_notification_email, + to: Gitlab::CurrentSettings.admin_notification_email, subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse" ) end @@ -15,6 +13,6 @@ class AbuseReportMailer < BaseMailer private def deliverable? - current_application_settings.admin_notification_email.present? + Gitlab::CurrentSettings.admin_notification_email.present? end end diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 8e99db444d6..654468bc7fe 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,13 +1,11 @@ class BaseMailer < ActionMailer::Base - include Gitlab::CurrentSettings - around_action :render_with_default_locale helper ApplicationHelper helper MarkupHelper attr_accessor :current_user - helper_method :current_user, :can?, :current_application_settings + helper_method :current_user, :can? default from: proc { default_sender_address.format } default reply_to: proc { default_reply_to_address.format } diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 9024f1df1cd..aa5cf97756f 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -17,8 +17,12 @@ module Clusters 'stable/nginx-ingress' end + def chart_values_file + "#{Rails.root}/vendor/#{name}/values.yaml" + end + def install_command - Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart) + Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file) end end end diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index 9160a169452..7f38dcc4a9c 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -1,7 +1,6 @@ module Clusters module Platforms class Kubernetes < ActiveRecord::Base - include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching @@ -169,7 +168,7 @@ module Clusters { token: token, ca_pem: ca_pem, - max_session_time: current_application_settings.terminal_max_session_time + max_session_time: Gitlab::CurrentSettings.terminal_max_session_time } end diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 99dbd4fbacf..b12c10a84de 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -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 diff --git a/app/models/group.rb b/app/models/group.rb index 62b1322ebe6..5b7f1b38612 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -274,12 +274,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 } diff --git a/app/models/issue_assignee.rb b/app/models/issue_assignee.rb index 06d760b6a89..326b9eb7ad5 100644 --- a/app/models/issue_assignee.rb +++ b/app/models/issue_assignee.rb @@ -1,6 +1,4 @@ class IssueAssignee < ActiveRecord::Base - extend Gitlab::CurrentSettings - belongs_to :issue belongs_to :assignee, class_name: "User", foreign_key: :user_id end diff --git a/app/models/key.rb b/app/models/key.rb index a3f8a5d6dc7..ae5769c0627 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -1,7 +1,6 @@ require 'digest/md5' class Key < ActiveRecord::Base - include Gitlab::CurrentSettings include AfterCommitQueue include Sortable @@ -34,9 +33,8 @@ class Key < ActiveRecord::Base after_destroy :refresh_user_cache def key=(value) - value&.delete!("\n\r") - value.strip! unless value.blank? - write_attribute(:key, value) + write_attribute(:key, value.present? ? Gitlab::SSHPublicKey.sanitize(value) : nil) + @public_key = nil end @@ -98,13 +96,13 @@ class Key < ActiveRecord::Base def generate_fingerprint self.fingerprint = nil - return unless self.key.present? + return unless public_key.valid? self.fingerprint = public_key.fingerprint end def key_meets_restrictions - restriction = current_application_settings.key_restriction_for(public_key.type) + restriction = Gitlab::CurrentSettings.key_restriction_for(public_key.type) if restriction == ApplicationSetting::FORBIDDEN_KEY_VALUE errors.add(:key, forbidden_key_type_message) @@ -115,7 +113,7 @@ class Key < ActiveRecord::Base def forbidden_key_type_message allowed_types = - current_application_settings + Gitlab::CurrentSettings .allowed_key_types .map(&:upcase) .to_sentence(last_word_connector: ', or ', two_words_connector: ' or ') diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 37a7417cafc..7b82d076975 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -2,7 +2,6 @@ class Namespace < ActiveRecord::Base include CacheMarkdownField include Sortable include Gitlab::ShellAdapter - include Gitlab::CurrentSettings include Gitlab::VisibilityLevel include Routable include AfterCommitQueue @@ -222,6 +221,24 @@ class Namespace < ActiveRecord::Base has_parent? 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 diff --git a/app/models/note.rb b/app/models/note.rb index a84db8982e5..01a778a7424 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -3,7 +3,6 @@ # A note of this type is never resolvable. class Note < ActiveRecord::Base extend ActiveModel::Naming - include Gitlab::CurrentSettings include Participable include Mentionable include Awardable @@ -196,7 +195,7 @@ class Note < ActiveRecord::Base end def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i + Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i end def hook_attrs diff --git a/app/models/project.rb b/app/models/project.rb index 90f5df6265d..12d5f28f5ea 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -4,7 +4,6 @@ class Project < ActiveRecord::Base include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Gitlab::CurrentSettings include AccessRequestable include Avatarable include CacheMarkdownField @@ -23,7 +22,6 @@ class Project < ActiveRecord::Base include ::Gitlab::Utils::StrongMemoize extend Gitlab::ConfigHelper - extend Gitlab::CurrentSettings BoardLimitExceeded = Class.new(StandardError) @@ -51,8 +49,8 @@ class Project < ActiveRecord::Base default_value_for :visibility_level, gitlab_config_features.visibility_level default_value_for :resolve_outdated_diff_discussions, false default_value_for :container_registry_enabled, gitlab_config_features.container_registry - default_value_for(:repository_storage) { current_application_settings.pick_repository_storage } - default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled } + default_value_for(:repository_storage) { Gitlab::CurrentSettings.pick_repository_storage } + default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled } default_value_for :issues_enabled, gitlab_config_features.issues default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests default_value_for :builds_enabled, gitlab_config_features.builds @@ -71,6 +69,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 @@ -486,14 +485,14 @@ class Project < ActiveRecord::Base def auto_devops_enabled? if auto_devops&.enabled.nil? - current_application_settings.auto_devops_enabled? + Gitlab::CurrentSettings.auto_devops_enabled? else auto_devops.enabled? end end def has_auto_devops_implicitly_disabled? - auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled? + auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled? end def empty_repo? @@ -1471,14 +1470,14 @@ class Project < ActiveRecord::Base # Ensure HEAD points to the default branch in case it is not master change_head(default_branch) - if current_application_settings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) + if Gitlab::CurrentSettings.default_branch_protection != Gitlab::Access::PROTECTION_NONE && !ProtectedBranch.protected?(self, default_branch) params = { name: default_branch, push_access_levels_attributes: [{ - access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER }], merge_access_levels_attributes: [{ - access_level: current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER }] } @@ -1527,6 +1526,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 @@ -1535,8 +1536,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 @@ -1773,7 +1775,7 @@ class Project < ActiveRecord::Base end def use_hashed_storage - if self.new_record? && current_application_settings.hashed_storage_enabled + if self.new_record? && Gitlab::CurrentSettings.hashed_storage_enabled self.storage_version = LATEST_STORAGE_VERSION end end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index c72b01b64af..e42fd802b92 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -4,7 +4,6 @@ # After we've migrated data, we'll remove KubernetesService. This would happen in a few months. # If you're modyfiyng this class, please note that you should update the same change in Clusters::Platforms::Kubernetes. class KubernetesService < DeploymentService - include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching @@ -231,7 +230,7 @@ class KubernetesService < DeploymentService { token: token, ca_pem: ca_pem, - max_session_time: current_application_settings.terminal_max_session_time + max_session_time: Gitlab::CurrentSettings.terminal_max_session_time } end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index d28fed11ca8..609780c5587 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -2,8 +2,6 @@ class ProtectedBranch < ActiveRecord::Base include Gitlab::ShellAdapter include ProtectedRef - extend Gitlab::CurrentSettings - protected_ref_access_levels :merge, :push # Check if branch name is marked as protected in the system @@ -16,7 +14,7 @@ class ProtectedBranch < ActiveRecord::Base end def self.default_branch_protected? - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || - current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE + Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_FULL || + Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE end end diff --git a/app/models/repository.rb b/app/models/repository.rb index edfb236a91a..f1abe5c3e07 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -173,15 +173,7 @@ class Repository end def find_branch(name, fresh_repo: true) - # Since the Repository object may have in-memory index changes, invalidating the memoized Repository object may - # cause unintended side effects. Because finding a branch is a read-only operation, we can safely instantiate - # a new repo here to ensure a consistent state to avoid a libgit2 bug where concurrent access (e.g. via git gc) - # may cause the branch to "disappear" erroneously or have the wrong SHA. - # - # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 - raw_repo = fresh_repo ? initialize_raw_repository : raw_repository - - raw_repo.find_branch(name) + raw_repository.find_branch(name, fresh_repo) end def find_tag(name) @@ -721,11 +713,11 @@ class Repository end def branch_names_contains(sha) - refs_contains_sha('branch', sha) + raw_repository.branch_names_contains_sha(sha) end def tag_names_contains(sha) - refs_contains_sha('tag', sha) + raw_repository.tag_names_contains_sha(sha) end def local_branches diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 05a16f11b59..7c8716f8c18 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -11,8 +11,6 @@ class Snippet < ActiveRecord::Base include Editable include Gitlab::SQL::Pattern - extend Gitlab::CurrentSettings - cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description cache_markdown_field :content @@ -28,7 +26,7 @@ class Snippet < ActiveRecord::Base default_content_html_invalidator || file_name_changed? end - default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility } + default_value_for(:visibility_level) { Gitlab::CurrentSettings.default_snippet_visibility } belongs_to :author, class_name: 'User' belongs_to :project diff --git a/app/models/user.rb b/app/models/user.rb index 89e787c3274..a57ba3740c9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -2,10 +2,8 @@ require 'carrierwave/orm/activerecord' class User < ActiveRecord::Base extend Gitlab::ConfigHelper - extend Gitlab::CurrentSettings include Gitlab::ConfigHelper - include Gitlab::CurrentSettings include Gitlab::SQL::Pattern include AfterCommitQueue include Avatarable @@ -30,7 +28,7 @@ class User < ActiveRecord::Base add_authentication_token_field :rss_token default_value_for :admin, false - default_value_for(:external) { current_application_settings.user_default_external } + default_value_for(:external) { Gitlab::CurrentSettings.user_default_external } default_value_for :can_create_group, gitlab_config.default_can_create_group default_value_for :can_create_team, false default_value_for :hide_no_ssh_key, false @@ -137,6 +135,7 @@ class User < ActiveRecord::Base has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent has_many :custom_attributes, class_name: 'UserCustomAttribute' + has_many :callouts, class_name: 'UserCallout' has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent # @@ -660,11 +659,11 @@ class User < ActiveRecord::Base end def allow_password_authentication_for_web? - current_application_settings.password_authentication_enabled_for_web? && !ldap_user? + Gitlab::CurrentSettings.password_authentication_enabled_for_web? && !ldap_user? end def allow_password_authentication_for_git? - current_application_settings.password_authentication_enabled_for_git? && !ldap_user? + Gitlab::CurrentSettings.password_authentication_enabled_for_git? && !ldap_user? end def can_change_username? @@ -792,7 +791,7 @@ class User < ActiveRecord::Base # without this safeguard! return unless has_attribute?(:projects_limit) && projects_limit.nil? - self.projects_limit = current_application_settings.default_projects_limit + self.projects_limit = Gitlab::CurrentSettings.default_projects_limit end def requires_ldap_check? @@ -1215,7 +1214,7 @@ class User < ActiveRecord::Base else # Only revert these back to the default if they weren't specifically changed in this update. self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed? - self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed? + self.projects_limit = Gitlab::CurrentSettings.default_projects_limit unless projects_limit_changed? end end @@ -1223,15 +1222,15 @@ class User < ActiveRecord::Base valid = true error = nil - if current_application_settings.domain_blacklist_enabled? - blocked_domains = current_application_settings.domain_blacklist + if Gitlab::CurrentSettings.domain_blacklist_enabled? + blocked_domains = Gitlab::CurrentSettings.domain_blacklist if domain_matches?(blocked_domains, email) error = 'is not from an allowed domain.' valid = false end end - allowed_domains = current_application_settings.domain_whitelist + allowed_domains = Gitlab::CurrentSettings.domain_whitelist unless allowed_domains.blank? if domain_matches?(allowed_domains, email) valid = true diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb new file mode 100644 index 00000000000..e4b69382626 --- /dev/null +++ b/app/models/user_callout.rb @@ -0,0 +1,13 @@ +class UserCallout < ActiveRecord::Base + belongs_to :user + + enum feature_name: { + gke_cluster_integration: 1 + } + + validates :user, presence: true + validates :feature_name, + presence: true, + uniqueness: { scope: :user_id }, + inclusion: { in: UserCallout.feature_names.keys } +end diff --git a/app/services/akismet_service.rb b/app/services/akismet_service.rb index aa6f0e841c9..0521393dd27 100644 --- a/app/services/akismet_service.rb +++ b/app/services/akismet_service.rb @@ -1,6 +1,4 @@ class AkismetService - include Gitlab::CurrentSettings - attr_accessor :owner, :text, :options def initialize(owner, text, options = {}) @@ -41,12 +39,12 @@ class AkismetService private def akismet_client - @akismet_client ||= ::Akismet::Client.new(current_application_settings.akismet_api_key, + @akismet_client ||= ::Akismet::Client.new(Gitlab::CurrentSettings.akismet_api_key, Gitlab.config.gitlab.url) end def akismet_enabled? - current_application_settings.akismet_enabled + Gitlab::CurrentSettings.akismet_enabled end def submit(type) diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index f40cd2b06c8..2b77f6be72a 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -1,7 +1,5 @@ module Auth class ContainerRegistryAuthenticationService < BaseService - extend Gitlab::CurrentSettings - AUDIENCE = 'container_registry'.freeze def execute(authentication_abilities:) @@ -32,7 +30,7 @@ module Auth end def self.token_expire_at - Time.now + current_application_settings.container_registry_token_expire_delay.minutes + Time.now + Gitlab::CurrentSettings.container_registry_token_expire_delay.minutes end private diff --git a/app/services/base_service.rb b/app/services/base_service.rb index a0cb00dba58..6883ba36c71 100644 --- a/app/services/base_service.rb +++ b/app/services/base_service.rb @@ -1,6 +1,5 @@ class BaseService include Gitlab::Allowable - include Gitlab::CurrentSettings attr_accessor :project, :current_user, :params diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index f832b79ef21..e09b445636f 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -2,8 +2,6 @@ module Ci # This class responsible for assigning # proper pending build to runner on runner API request class RegisterJobService - include Gitlab::CurrentSettings - attr_reader :runner Result = Struct.new(:build, :valid?) diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index e6fd193ffb3..c037141fcde 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -1,6 +1,5 @@ class GitPushService < BaseService attr_accessor :push_data, :push_commits - include Gitlab::CurrentSettings include Gitlab::Access # The N most recent commits to process in a single push payload. diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb index e77e08aa380..c6e52c3bb91 100644 --- a/app/services/gravatar_service.rb +++ b/app/services/gravatar_service.rb @@ -1,8 +1,6 @@ class GravatarService - include Gitlab::CurrentSettings - def execute(email, size = nil, scale = 2, username: nil) - return unless current_application_settings.gravatar_enabled? + return unless Gitlab::CurrentSettings.gravatar_enabled? identifier = email.presence || username.presence return unless identifier diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 22b9b91a957..2ae855d078b 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -1,7 +1,9 @@ module MergeRequests class BuildService < MergeRequests::BaseService + include Gitlab::Utils::StrongMemoize + def execute - @issue_iid = params.delete(:issue_iid) + @params_issue_iid = params.delete(:issue_iid) self.merge_request = MergeRequest.new(params) merge_request.compare_commits = [] @@ -123,7 +125,7 @@ module MergeRequests # def assign_title_and_description assign_title_and_description_from_single_commit - assign_title_from_issue + assign_title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker merge_request.title ||= source_branch.titleize.humanize merge_request.title = wip_title if compare_commits.empty? @@ -132,9 +134,9 @@ module MergeRequests end def append_closes_description - return unless issue_iid + return unless issue - closes_issue = "Closes ##{issue_iid}" + closes_issue = "Closes #{issue.to_reference}" if description.present? merge_request.description += closes_issue.prepend("\n\n") @@ -154,13 +156,27 @@ module MergeRequests end def assign_title_from_issue - return unless issue && issue.is_a?(Issue) + return unless issue + + merge_request.title = "Resolve \"#{issue.title}\"" if issue.is_a?(Issue) - merge_request.title = "Resolve \"#{issue.title}\"" + unless merge_request.title + branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize + merge_request.title = "Resolve #{issue_iid}" + merge_request.title += " \"#{branch_title}\"" unless branch_title.empty? + end end def issue_iid - @issue_iid ||= source_branch.match(/\A(\d+)-/).try(:[], 1) + strong_memoize(:issue_iid) do + @params_issue_iid || begin + id = if target_project.external_issue_tracker + source_branch.match(target_project.external_issue_reference_pattern).try(:[], 0) + end + + id || source_branch.match(/\A(\d+)-/).try(:[], 1) + end + end end def issue diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 262622f8bd0..18c40ce8992 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -90,6 +90,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 def reset_merge_when_pipeline_succeeds @@ -195,7 +199,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 diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb index dcef8b66215..120d57a188d 100644 --- a/app/services/projects/housekeeping_service.rb +++ b/app/services/projects/housekeeping_service.rb @@ -7,8 +7,6 @@ # module Projects class HousekeepingService < BaseService - include Gitlab::CurrentSettings - # Timeout set to 24h LEASE_TIMEOUT = 86400 @@ -83,19 +81,19 @@ module Projects end def housekeeping_enabled? - current_application_settings.housekeeping_enabled + Gitlab::CurrentSettings.housekeeping_enabled end def gc_period - current_application_settings.housekeeping_gc_period + Gitlab::CurrentSettings.housekeeping_gc_period end def full_repack_period - current_application_settings.housekeeping_full_repack_period + Gitlab::CurrentSettings.housekeeping_full_repack_period end def repack_period - current_application_settings.housekeeping_incremental_repack_period + Gitlab::CurrentSettings.housekeeping_incremental_repack_period end end end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index a773222bf17..c760bd3b626 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -1,7 +1,5 @@ module Projects class UpdatePagesService < BaseService - include Gitlab::CurrentSettings - BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte SITE_PATH = 'public/'.freeze @@ -134,7 +132,7 @@ module Projects end def max_size - max_pages_size = current_application_settings.max_pages_size.megabytes + max_pages_size = Gitlab::CurrentSettings.max_pages_size.megabytes return MAX_SIZE if max_pages_size.zero? diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index ff4c73c886e..0e235a6d2a0 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -34,7 +34,7 @@ module Projects def run_auto_devops_pipeline? return false if project.repository.gitlab_ci_yml || !project.auto_devops.previous_changes.include?('enabled') - project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && current_application_settings.auto_devops_enabled?) + project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?) end private diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 14171bce782..2623f253d98 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -11,10 +11,8 @@ class SubmitUsagePingService percentage_projects_prometheus_active leader_service_desk_issues instance_service_desk_issues percentage_service_desk_issues].freeze - include Gitlab::CurrentSettings - def execute - return false unless current_application_settings.usage_ping_enabled? + return false unless Gitlab::CurrentSettings.usage_ping_enabled? response = HTTParty.post( URL, diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 06b23cd7076..2253d638e93 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -22,8 +22,7 @@ module SystemNoteService commits_text = "#{total_count} commit".pluralize(total_count) body = "added #{commits_text}\n\n" - body << existing_commit_summary(noteable, existing_commits, oldrev) - body << new_commit_summary(new_commits).join("\n") + body << commits_list(noteable, new_commits, existing_commits, oldrev) body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})" create_note(NoteSummary.new(noteable, project, author, body, action: 'commit', commit_count: total_count)) @@ -481,7 +480,7 @@ module SystemNoteService # Returns an Array of Strings def new_commit_summary(new_commits) new_commits.collect do |commit| - "* #{commit.short_id} - #{escape_html(commit.title)}" + content_tag('li', "#{commit.short_id} - #{commit.title}") end end @@ -604,6 +603,16 @@ module SystemNoteService "#{cross_reference_note_prefix}#{gfm_reference}" end + # Builds a list of existing and new commits according to existing_commits and + # new_commits methods. + # Returns a String wrapped in `ul` and `li` tags. + def commits_list(noteable, new_commits, existing_commits, oldrev) + existing_commit_summary = existing_commit_summary(noteable, existing_commits, oldrev) + new_commit_summary = new_commit_summary(new_commits).join + + content_tag('ul', "#{existing_commit_summary}#{new_commit_summary}".html_safe) + end + # Build a single line summarizing existing commits being added in a merge # request # @@ -640,11 +649,8 @@ module SystemNoteService branch = noteable.target_branch branch = "#{noteable.target_project_namespace}:#{branch}" if noteable.for_fork? - "* #{commit_ids} - #{commits_text} from branch `#{branch}`\n" - end - - def escape_html(text) - Rack::Utils.escape_html(text) + branch_name = content_tag('code', branch) + content_tag('li', "#{commit_ids} - #{commits_text} from branch #{branch_name}".html_safe) end def url_helpers @@ -661,4 +667,8 @@ module SystemNoteService start_sha: oldrev ) end + + def content_tag(*args) + ActionController::Base.helpers.content_tag(*args) + end end diff --git a/app/services/upload_service.rb b/app/services/upload_service.rb index 76700dfcdee..d5a9b344905 100644 --- a/app/services/upload_service.rb +++ b/app/services/upload_service.rb @@ -1,6 +1,4 @@ class UploadService - include Gitlab::CurrentSettings - def initialize(model, file, uploader_class = FileUploader) @model, @file, @uploader_class = model, file, uploader_class end @@ -17,6 +15,6 @@ class UploadService private def max_attachment_size - current_application_settings.max_attachment_size.megabytes.to_i + Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i end end diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb index 61f1568f366..4fb6d221909 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -1,7 +1,5 @@ module Users class BuildService < BaseService - include Gitlab::CurrentSettings - def initialize(current_user, params = {}) @current_user = current_user @params = params.dup @@ -34,7 +32,7 @@ module Users private def can_create_user? - (current_user.nil? && current_application_settings.allow_signup?) || current_user&.admin? + (current_user.nil? && Gitlab::CurrentSettings.allow_signup?) || current_user&.admin? end # Allowed params for creating a user (admins only) @@ -102,7 +100,7 @@ module Users end def skip_user_confirmation_email_from_setting - !current_application_settings.send_user_confirmation_email + !Gitlab::CurrentSettings.send_user_confirmation_email end end end diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml deleted file mode 100644 index c72e59640d7..00000000000 --- a/app/views/admin/broadcast_messages/preview.js.haml +++ /dev/null @@ -1 +0,0 @@ -$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}"); diff --git a/app/views/admin/conversational_development_index/show.html.haml b/app/views/admin/conversational_development_index/show.html.haml index 30dd87f0463..ed40e7b4d00 100644 --- a/app/views/admin/conversational_development_index/show.html.haml +++ b/app/views/admin/conversational_development_index/show.html.haml @@ -6,7 +6,7 @@ = render 'callout' .prepend-top-default - - if !current_application_settings.usage_ping_enabled + - if !Gitlab::CurrentSettings.usage_ping_enabled = render 'disabled' - elsif @metric.blank? = render 'no_data' diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index d251f75a8fd..e3711421b61 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -119,7 +119,7 @@ .well-segment.admin-well %h4 Components - - if current_application_settings.version_check_enabled + - if Gitlab::CurrentSettings.version_check_enabled .pull-right = version_status_badge %p diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml index 10a3bed0a4f..e31fb58b205 100644 --- a/app/views/admin/health_check/show.html.haml +++ b/app/views/admin/health_check/show.html.haml @@ -8,7 +8,7 @@ .pull-left %p #{ s_('HealthCheck|Access token is') } - %code#health-check-token= current_application_settings.health_check_access_token + %code#health-check-token= Gitlab::CurrentSettings.health_check_access_token .prepend-top-10 = button_to _("Reset health check access token"), reset_health_check_token_admin_application_settings_path, method: :put, class: 'btn btn-default', @@ -18,11 +18,11 @@ = link_to s_('More information is available|here'), help_page_path('user/admin_area/monitoring/health_check') %ul %li - %code= readiness_url(token: current_application_settings.health_check_access_token) + %code= readiness_url(token: Gitlab::CurrentSettings.health_check_access_token) %li - %code= liveness_url(token: current_application_settings.health_check_access_token) + %code= liveness_url(token: Gitlab::CurrentSettings.health_check_access_token) %li - %code= metrics_url(token: current_application_settings.health_check_access_token) + %code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token) %hr .panel.panel-default diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 4f60be698e9..1e52646b1cc 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -36,7 +36,7 @@ data: { confirm: _("Are you sure you want to reset registration token?") } = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: current_application_settings.runners_registration_token, + locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token, type: 'shared' } .append-bottom-20.clearfix diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml new file mode 100644 index 00000000000..495a55660cb --- /dev/null +++ b/app/views/ci/variables/_variable_row.html.haml @@ -0,0 +1,49 @@ +- form_field = local_assigns.fetch(:form_field, nil) +- variable = local_assigns.fetch(:variable, nil) +- only_key_value = local_assigns.fetch(:only_key_value, false) + +- id = variable&.id +- key = variable&.key +- value = variable&.value +- is_protected = variable && !only_key_value ? variable.protected : true + +- id_input_name = "#{form_field}[variables_attributes][][id]" +- destroy_input_name = "#{form_field}[variables_attributes][][_destroy]" +- key_input_name = "#{form_field}[variables_attributes][][key]" +- value_input_name = "#{form_field}[variables_attributes][][value]" +- protected_input_name = "#{form_field}[variables_attributes][][protected]" + +%li.js-row.ci-variable-row{ data: { is_persisted: "#{!id.nil?}" } } + .ci-variable-row-body + %input.js-ci-variable-input-id{ type: "hidden", name: id_input_name, value: id } + %input.js-ci-variable-input-destroy{ type: "hidden", name: destroy_input_name } + %input.js-ci-variable-input-key.ci-variable-body-item.form-control{ type: "text", + name: key_input_name, + value: key, + placeholder: s_('CiVariables|Input variable key') } + .ci-variable-body-item + .form-control.js-secret-value-placeholder{ class: ('hide' unless id) } + = '*' * 20 + %textarea.js-ci-variable-input-value.js-secret-value.form-control{ class: ('hide' if id), + rows: 1, + name: value_input_name, + placeholder: s_('CiVariables|Input variable value') } + = value + - unless only_key_value + .ci-variable-body-item.ci-variable-protected-item + .append-right-default + = s_("CiVariable|Protected") + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if is_protected}", + "aria-label": s_("CiVariable|Toggle protected") } + %input{ type: "hidden", + class: 'js-ci-variable-input-protected js-project-feature-toggle-input', + name: protected_input_name, + value: is_protected } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + -# EE-specific start + -# EE-specific end + %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') } + = icon('minus-circle') diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index cebdbab4e74..617c20b9635 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -1,5 +1,5 @@ .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 diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 9038c4fbebd..449a2ce625e 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -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 diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml index c18077bc66f..97f854cc5f0 100644 --- a/app/views/dashboard/projects/_nav.html.haml +++ b/app/views/dashboard/projects/_nav.html.haml @@ -1,5 +1,5 @@ .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 diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 20ca6ec969a..664966989db 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -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 diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml index fb70d158096..79826a364db 100644 --- a/app/views/devise/confirmations/almost_there.haml +++ b/app/views/devise/confirmations/almost_there.haml @@ -4,9 +4,9 @@ %p.lead.append-bottom-20 Please check your email to confirm your account %hr -- if current_application_settings.after_sign_up_text.present? +- if Gitlab::CurrentSettings.after_sign_up_text.present? .well-confirmation.text-center - = markdown_field(current_application_settings, :after_sign_up_text) + = markdown_field(Gitlab::CurrentSettings, :after_sign_up_text) %p.text-center No confirmation email received? Please check your spam folder or .append-bottom-20.prepend-top-20.text-center diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml index fdd72ead2cb..63811ea1c81 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -1,8 +1,8 @@ = webpack_bundle_tag 'docs' %div -- if current_application_settings.help_page_text.present? - = markdown_field(current_application_settings, :help_page_text) +- if Gitlab::CurrentSettings.help_page_text.present? + = markdown_field(Gitlab::CurrentSettings.current_application_settings, :help_page_text) %hr %h1 @@ -14,7 +14,7 @@ = version_status_badge %hr -- unless current_application_settings.help_page_hide_commercial_content? +- unless Gitlab::CurrentSettings.help_page_hide_commercial_content? %p.slead GitLab is open source software to collaborate on code. %br @@ -46,6 +46,6 @@ %li %button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' } Use shortcuts - - unless current_application_settings.help_page_hide_commercial_content? + - unless Gitlab::CurrentSettings.help_page_hide_commercial_content? %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/' %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare' diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml index 04e2d4b63e6..bb7f9ba7ae4 100644 --- a/app/views/koding/index.html.haml +++ b/app/views/koding/index.html.haml @@ -3,4 +3,4 @@ = icon('circle', class: 'cgreen') Integration is active for = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do - #{current_application_settings.koding_url} + #{Gitlab::CurrentSettings.koding_url} diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index ea13a5e6d62..0c979109b3f 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -41,12 +41,14 @@ = webpack_bundle_tag "webpack_runtime" = webpack_bundle_tag "common" = webpack_bundle_tag "main" - = webpack_bundle_tag "raven" if current_application_settings.clientside_sentry_enabled + = webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled = webpack_bundle_tag "test" if Rails.env.test? - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts + = webpack_controller_bundle_tags + = yield :project_javascripts = csrf_meta_tags diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index 4e9ea33e675..257f7326409 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -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 @@ -26,8 +26,8 @@ Perform code reviews and enhance collaboration with merge requests. Each project can also have an issue tracker and a wiki. - - if current_application_settings.sign_in_text.present? - = markdown_field(current_application_settings, :sign_in_text) + - if Gitlab::CurrentSettings.sign_in_text.present? + = markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text) %hr.footer-fixed .container.footer-container diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index a5a62a0695f..c878fcf2808 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -28,7 +28,7 @@ = link_to profile_account_path do %strong.fly-out-top-item-name #{ _('Account') } - - if current_application_settings.user_oauth_applications? + - if Gitlab::CurrentSettings.user_oauth_applications? = nav_link(controller: 'oauth/applications') do = link_to applications_profile_path do .nav-icon-container diff --git a/app/views/notify/_note_email.html.haml b/app/views/notify/_note_email.html.haml index 3e36da31ea3..94bd6f96dbc 100644 --- a/app/views/notify/_note_email.html.haml +++ b/app/views/notify/_note_email.html.haml @@ -22,7 +22,7 @@ - else commented on a #{link_to 'discussion', @target_url} -- elsif current_application_settings.email_author_in_body +- elsif Gitlab::CurrentSettings.email_author_in_body %p.details #{link_to @note.author_name, user_url(@note.author)} commented: diff --git a/app/views/notify/_note_email.text.erb b/app/views/notify/_note_email.text.erb index cb2e7fab6d5..c319cb55e87 100644 --- a/app/views/notify/_note_email.text.erb +++ b/app/views/notify/_note_email.text.erb @@ -12,7 +12,7 @@ <%= ":" -%> -<% elsif current_application_settings.email_author_in_body -%> +<% elsif Gitlab::CurrentSettings.email_author_in_body -%> <%= "#{@note.author_name} commented:" -%> diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml index eb5157ccac9..e6cdaf85c0d 100644 --- a/app/views/notify/new_issue_email.html.haml +++ b/app/views/notify/new_issue_email.html.haml @@ -1,4 +1,4 @@ -- if current_application_settings.email_author_in_body +- if Gitlab::CurrentSettings.email_author_in_body %p.details #{link_to @issue.author_name, user_url(@issue.author)} created an issue: diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index 951c96bdb9c..0a9adc6f243 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -1,4 +1,4 @@ -- if current_application_settings.email_author_in_body +- if Gitlab::CurrentSettings.email_author_in_body %p.details #{link_to @merge_request.author_name, user_url(@merge_request.author)} created a merge request: diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml index 00e1b5faae3..db4424a01f9 100644 --- a/app/views/notify/new_user_email.html.haml +++ b/app/views/notify/new_user_email.html.haml @@ -1,7 +1,7 @@ %p Hi #{@user['name']}! %p - - if current_application_settings.allow_signup? + - if Gitlab::CurrentSettings.allow_signup? Your account has been created successfully. - else The Administrator created an account for you. Now you are a member of the company GitLab application. diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index e759c87bda7..5dfe973f33c 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -1,4 +1,4 @@ -- return unless current_application_settings.project_export_enabled? +- return unless Gitlab::CurrentSettings.project_export_enabled? - project = local_assigns.fetch(:project) - expanded = Rails.env.test? diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 90272ad9554..64259669c19 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -6,7 +6,7 @@ - link = commit_path(project, commit, merge_request: merge_request) - cache_key = [project.full_path, commit.id, - current_application_settings, + Gitlab::CurrentSettings.current_application_settings, @path.presence, current_controller?(:commits), merge_request&.iid, diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 857ae00d0ab..ff440e99042 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -22,14 +22,20 @@ = f.label :ref, _('Target Branch'), class: 'label-light' = dropdown_tag(_("Select target branch"), options: { toggle_class: 'btn js-target-branch-dropdown', dropdown_class: 'git-revision-dropdown', title: _("Select target branch"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: @project.repository.branch_names, default_branch: @project.default_branch } } ) = f.text_field :ref, value: @schedule.ref, id: 'schedule_ref', class: 'hidden', name: 'schedule[ref]', required: true - .form-group + .form-group.js-ci-variable-list-section .col-md-9 %label.label-light #{ s_('PipelineSchedules|Variables') } - %ul.js-pipeline-variable-list.pipeline-variable-list - - @schedule.variables.each do |variable| - = render 'variable_row', id: variable.id, key: variable.key, value: variable.value - = render 'variable_row' + %ul.ci-variable-list + - @schedule.variables.each do |variable| + = render 'ci/variables/variable_row', form_field: 'schedule', variable: variable, only_key_value: true + = render 'ci/variables/variable_row', form_field: 'schedule', only_key_value: true + - if @schedule.variables.size > 0 + %button.btn.btn-info.btn-inverted.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: "#{@schedule.variables.size == 0}" } } + - if @schedule.variables.size == 0 + = n_('Hide value', 'Hide values', @schedule.variables.size) + - else + = n_('Reveal value', 'Reveal values', @schedule.variables.size) .form-group .col-md-9 = f.label :active, s_('PipelineSchedules|Activated'), class: 'label-light' diff --git a/app/views/projects/pipeline_schedules/_tabs.html.haml b/app/views/projects/pipeline_schedules/_tabs.html.haml index 7fcb624a9dd..8996c1b3e38 100644 --- a/app/views/projects/pipeline_schedules/_tabs.html.haml +++ b/app/views/projects/pipeline_schedules/_tabs.html.haml @@ -1,4 +1,4 @@ -%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") diff --git a/app/views/projects/pipeline_schedules/_variable_row.html.haml b/app/views/projects/pipeline_schedules/_variable_row.html.haml deleted file mode 100644 index 564cb5d1ca9..00000000000 --- a/app/views/projects/pipeline_schedules/_variable_row.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- id = local_assigns.fetch(:id, nil) -- key = local_assigns.fetch(:key, "") -- value = local_assigns.fetch(:value, "") -%li.js-row.pipeline-variable-row{ data: { is_persisted: "#{!id.nil?}" } } - .pipeline-variable-row-body - %input{ type: "hidden", name: "schedule[variables_attributes][][id]", value: id } - %input.js-destroy-input{ type: "hidden", name: "schedule[variables_attributes][][_destroy]" } - %input.js-user-input.pipeline-variable-key-input.form-control{ type: "text", - name: "schedule[variables_attributes][][key]", - value: key, - placeholder: s_('PipelineSchedules|Input variable key') } - %textarea.js-user-input.pipeline-variable-value-input.form-control{ rows: 1, - name: "schedule[variables_attributes][][value]", - placeholder: s_('PipelineSchedules|Input variable value') } - = value - %button.js-row-remove-button.pipeline-variable-row-remove-button{ 'aria-label': s_('PipelineSchedules|Remove variable row') } - %i.fa.fa-minus-circle{ 'aria-hidden': "true" } diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 398a1c46746..5de17977d5a 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -1,7 +1,7 @@ - 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 diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml index c5f9f5aa15b..646c01c0989 100644 --- a/app/views/projects/pipelines_settings/_show.html.haml +++ b/app/views/projects/pipelines_settings/_show.html.haml @@ -31,7 +31,7 @@ .radio = form.label :enabled_ do = form.radio_button :enabled, '' - %strong Instance default (#{current_application_settings.auto_devops_enabled? ? 'enabled' : 'disabled'}) + %strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'}) %br %span.descr Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>. diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml index 67607e4e9c6..b037b57e78a 100644 --- a/app/views/projects/runners/_shared_runners.html.haml +++ b/app/views/projects/runners/_shared_runners.html.haml @@ -1,8 +1,8 @@ %h3 Shared Runners .bs-callout.bs-callout-warning.shared-runners-description - - if current_application_settings.shared_runners_text.present? - = markdown_field(current_application_settings, :shared_runners_text) + - if Gitlab::CurrentSettings.shared_runners_text.present? + = markdown_field(Gitlab::CurrentSettings.current_application_settings, :shared_runners_text) - else GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 151aad306a0..e7fa7477e0c 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,11 +1,14 @@ -%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') diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml index db2ac1e1d12..034b76b978f 100644 --- a/app/views/shared/_milestones_filter.html.haml +++ b/app/views/shared/_milestones_filter.html.haml @@ -1,4 +1,4 @@ -%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 diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml index 639f28cc210..0b003125912 100644 --- a/app/views/shared/builds/_tabs.html.haml +++ b/app/views/shared/builds/_tabs.html.haml @@ -1,4 +1,4 @@ -%ul.nav-links +%ul.nav-links.mobile-separator %li{ class: active_when(scope.nil?) }> = link_to build_path_proc.call(nil) do All diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml index 6d8a4668cec..4d8109eb90c 100644 --- a/app/views/shared/issuable/_nav.html.haml +++ b/app/views/shared/issuable/_nav.html.haml @@ -1,7 +1,7 @@ - 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)} diff --git a/app/views/snippets/_snippets_scope_menu.html.haml b/app/views/snippets/_snippets_scope_menu.html.haml index 8b6a98a054a..65aa4fbc757 100644 --- a/app/views/snippets/_snippets_scope_menu.html.haml +++ b/app/views/snippets/_snippets_scope_menu.html.haml @@ -1,7 +1,7 @@ - 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 diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index 8e26275669e..7ba224d74c8 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -1,6 +1,5 @@ class GitGarbageCollectWorker include ApplicationWorker - include Gitlab::CurrentSettings sidekiq_options retry: false @@ -102,7 +101,7 @@ class GitGarbageCollectWorker end def bitmaps_enabled? - current_application_settings.housekeeping_bitmaps_enabled + Gitlab::CurrentSettings.housekeeping_bitmaps_enabled end def git(write_bitmaps:) diff --git a/changelogs/unreleased/24167__color_label.yml b/changelogs/unreleased/24167__color_label.yml new file mode 100644 index 00000000000..68c6c731163 --- /dev/null +++ b/changelogs/unreleased/24167__color_label.yml @@ -0,0 +1,5 @@ +--- +title: Add Colors to GitLab Flavored Markdown +merge_request: 16095 +author: Tony Rom <thetonyrom@gmail.com> +type: added diff --git a/changelogs/unreleased/25327-coverage-badge-rounding.yml b/changelogs/unreleased/25327-coverage-badge-rounding.yml new file mode 100644 index 00000000000..ea985689484 --- /dev/null +++ b/changelogs/unreleased/25327-coverage-badge-rounding.yml @@ -0,0 +1,5 @@ +--- +title: Show coverage to two decimal points in coverage badge +merge_request: 10083 +author: Jeff Stubler +type: changed diff --git a/changelogs/unreleased/32283-trending-projects-unique-constraint2.yml b/changelogs/unreleased/32283-trending-projects-unique-constraint2.yml new file mode 100644 index 00000000000..4fd6b6fddc4 --- /dev/null +++ b/changelogs/unreleased/32283-trending-projects-unique-constraint2.yml @@ -0,0 +1,5 @@ +--- +title: Add unique constraint to trending_projects#project_id. +merge_request: 16846 +author: +type: other diff --git a/changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml b/changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml new file mode 100644 index 00000000000..5c45d0db602 --- /dev/null +++ b/changelogs/unreleased/39985-enable-prometheus-metrics-for-deployed-ingresses.yml @@ -0,0 +1,5 @@ +--- +title: Enable Prometheus metrics for deployed Ingresses +merge_request: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16866 +author: joshlambert +type: changed diff --git a/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml new file mode 100644 index 00000000000..9e4811ca308 --- /dev/null +++ b/changelogs/unreleased/40552-sanitize-extra-blank-spaces-used-when-uploading-a-ssh-key.yml @@ -0,0 +1,5 @@ +--- +title: Sanitize extra blank spaces used when uploading a SSH key +merge_request: 40552 +author: +type: fixed diff --git a/changelogs/unreleased/40793-fix-mr-title-for-jira.yml b/changelogs/unreleased/40793-fix-mr-title-for-jira.yml new file mode 100644 index 00000000000..69461510a87 --- /dev/null +++ b/changelogs/unreleased/40793-fix-mr-title-for-jira.yml @@ -0,0 +1,5 @@ +--- +title: Prevent JIRA issue identifier from being humanized. +merge_request: 16491 +author: Andrew McCallum +type: fixed diff --git a/changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml b/changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml new file mode 100644 index 00000000000..d7a8b6e6f81 --- /dev/null +++ b/changelogs/unreleased/42270-fix-namespace-remove-exports-for-hashed-storage.yml @@ -0,0 +1,6 @@ +--- +title: Fix export removal for hashed-storage projects within a renamed or deleted + namespace +merge_request: 16658 +author: +type: fixed diff --git a/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml b/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml new file mode 100644 index 00000000000..c46cc86c99b --- /dev/null +++ b/changelogs/unreleased/42696-gitlab-import-leaves-group_id-on-projectlabel.yml @@ -0,0 +1,5 @@ +--- +title: Fix GitLab import leaving group_id on ProjectLabel +merge_request: 16877 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml b/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml new file mode 100644 index 00000000000..378f0ef7ce9 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-500-on-fork-without-restricted-visibility-levels.yml @@ -0,0 +1,5 @@ +--- +title: Fix forking projects when no restricted visibility levels are defined applicationwide +merge_request: 16881 +author: +type: fixed diff --git a/changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml b/changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml new file mode 100644 index 00000000000..b79ec2944fd --- /dev/null +++ b/changelogs/unreleased/fix-adjust-button-group-width-on-mobile.yml @@ -0,0 +1,5 @@ +--- +title: Change button group width on mobile +merge_request: 16726 +author: George Tsiolis +type: fixed diff --git a/changelogs/unreleased/issue-42689-new-file-template.yml b/changelogs/unreleased/issue-42689-new-file-template.yml new file mode 100644 index 00000000000..d6b77b87605 --- /dev/null +++ b/changelogs/unreleased/issue-42689-new-file-template.yml @@ -0,0 +1,5 @@ +--- +title: Trigger change event on filename input when file template is applied +merge_request: 16911 +author: Sebastian Klingler +type: fixed diff --git a/changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml b/changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml new file mode 100644 index 00000000000..b2c1cd9710a --- /dev/null +++ b/changelogs/unreleased/osw-markdown-bypass-for-commit-messages.yml @@ -0,0 +1,5 @@ +--- +title: Bypass commits title markdown on notes +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/osw-system-notes-for-commits-regression.yml b/changelogs/unreleased/osw-system-notes-for-commits-regression.yml new file mode 100644 index 00000000000..6d0943c7716 --- /dev/null +++ b/changelogs/unreleased/osw-system-notes-for-commits-regression.yml @@ -0,0 +1,5 @@ +--- +title: Reload MRs memoization after diffs creation +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/persistent-callouts.yml b/changelogs/unreleased/persistent-callouts.yml new file mode 100644 index 00000000000..ca949a3b96c --- /dev/null +++ b/changelogs/unreleased/persistent-callouts.yml @@ -0,0 +1,5 @@ +--- +title: Add backend for persistently dismissably callouts +merge_request: +author: +type: added diff --git a/changelogs/unreleased/query-counts.yml b/changelogs/unreleased/query-counts.yml new file mode 100644 index 00000000000..e01ff8a4ad8 --- /dev/null +++ b/changelogs/unreleased/query-counts.yml @@ -0,0 +1,5 @@ +--- +title: Track and act upon the number of executed queries +merge_request: +author: +type: added diff --git a/changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml b/changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml new file mode 100644 index 00000000000..d43675e175d --- /dev/null +++ b/changelogs/unreleased/refactor-ci-variable-list-for-future-usage-in-4110.yml @@ -0,0 +1,5 @@ +--- +title: Hide variable values on pipeline schedule edit page +merge_request: 16729 +author: +type: changed diff --git a/config/initializers/query_limiting.rb b/config/initializers/query_limiting.rb new file mode 100644 index 00000000000..66864d1898e --- /dev/null +++ b/config/initializers/query_limiting.rb @@ -0,0 +1,9 @@ +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 diff --git a/config/routes.rb b/config/routes.rb index f162043dd5e..e72ea1881cd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -60,6 +60,9 @@ Rails.application.routes.draw do resources :issues, module: :boards, only: [:index, :update] end + + # UserCallouts + resources :user_callouts, only: [:create] end # Koding route diff --git a/config/webpack.config.js b/config/webpack.config.js index 783677b5b8d..7f3fe551a03 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -3,6 +3,7 @@ var crypto = require('crypto'); var fs = require('fs'); var path = require('path'); +var glob = require('glob'); var webpack = require('webpack'); var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; var CopyWebpackPlugin = require('copy-webpack-plugin'); @@ -20,6 +21,26 @@ var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false'; var WEBPACK_REPORT = process.env.WEBPACK_REPORT; var NO_COMPRESSION = process.env.NO_COMPRESSION; +// generate automatic entry points +var autoEntries = {}; +var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') }); + +// filter out entries currently imported dynamically in dispatcher.js +var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString(); +var dispatcherChunks = dispatcher.match(/(?!import\('.\/)pages\/[^']+/g); + +pageEntries.forEach(( path ) => { + let chunkPath = path.replace(/\/index\.js$/, ''); + if (!dispatcherChunks.includes(chunkPath)) { + let chunkName = chunkPath.replace(/\//g, '.'); + autoEntries[chunkName] = './' + path; + } +}); + +// report our auto-generated bundle count +var autoEntriesCount = Object.keys(autoEntries).length; +console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`); + var config = { // because sqljs requires fs. node: { @@ -301,6 +322,8 @@ var config = { } } +config.entry = Object.assign({}, autoEntries, config.entry); + if (IS_PRODUCTION) { config.devtool = 'source-map'; config.plugins.push( diff --git a/db/migrate/20180125214301_create_user_callouts.rb b/db/migrate/20180125214301_create_user_callouts.rb new file mode 100644 index 00000000000..856eff36ae0 --- /dev/null +++ b/db/migrate/20180125214301_create_user_callouts.rb @@ -0,0 +1,16 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class CreateUserCallouts < ActiveRecord::Migration + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + create_table :user_callouts do |t| + t.integer :feature_name, null: false + t.references :user, index: true, foreign_key: { on_delete: :cascade }, null: false + end + + add_index :user_callouts, [:user_id, :feature_name], unique: true + end +end diff --git a/db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb b/db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb new file mode 100644 index 00000000000..02e53b8fa8a --- /dev/null +++ b/db/migrate/20180201102129_add_unique_constraint_to_trending_projects_project_id.rb @@ -0,0 +1,19 @@ +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 diff --git a/db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb b/db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb new file mode 100644 index 00000000000..2f339172eeb --- /dev/null +++ b/db/post_migrate/20171207150300_remove_project_labels_group_id_copy.rb @@ -0,0 +1,21 @@ +# Copy of 20180202111106 - this one should run before 20171207150343 to fix issues related to +# the removal of groups with labels. + +class RemoveProjectLabelsGroupIdCopy < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + # rubocop:disable Migration/UpdateColumnInBatches + 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 + # rubocop:enable Migration/UpdateColumnInBatches + end + + def down + end +end diff --git a/db/post_migrate/20180202111106_remove_project_labels_group_id.rb b/db/post_migrate/20180202111106_remove_project_labels_group_id.rb new file mode 100644 index 00000000000..db7fd0d167d --- /dev/null +++ b/db/post_migrate/20180202111106_remove_project_labels_group_id.rb @@ -0,0 +1,19 @@ +# 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 diff --git a/db/schema.rb b/db/schema.rb index b09e06726f8..50a2ceaaeee 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" @@ -1727,7 +1727,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" @@ -1771,6 +1771,14 @@ ActiveRecord::Schema.define(version: 20180201145907) do add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree + create_table "user_callouts", force: :cascade do |t| + t.integer "feature_name", null: false + t.integer "user_id", null: false + end + + add_index "user_callouts", ["user_id", "feature_name"], name: "index_user_callouts_on_user_id_and_feature_name", unique: true, using: :btree + add_index "user_callouts", ["user_id"], name: "index_user_callouts_on_user_id", using: :btree + create_table "user_custom_attributes", force: :cascade do |t| t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "updated_at", null: false @@ -2042,6 +2050,7 @@ ActiveRecord::Schema.define(version: 20180201145907) do add_foreign_key "todos", "projects", name: "fk_45054f9c45", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" + add_foreign_key "user_callouts", "users", on_delete: :cascade add_foreign_key "user_custom_attributes", "users", on_delete: :cascade add_foreign_key "user_synced_attributes_metadata", "users", on_delete: :cascade add_foreign_key "users_star_projects", "projects", name: "fk_22cd27ddfc", on_delete: :cascade diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md index 9bcd13a52f7..e6c8f59549f 100644 --- a/doc/administration/environment_variables.md +++ b/doc/administration/environment_variables.md @@ -13,7 +13,7 @@ override certain values. Variable | Type | Description -------- | ---- | ----------- -`GITLAB_CDN_HOST` | string | Sets the hostname for a CDN to serve static assets (e.g. `mycdnsubdomain.fictional-cdn.com`) +`GITLAB_CDN_HOST` | string | Sets the base URL for a CDN to serve static assets (e.g. `//mycdnsubdomain.fictional-cdn.com`) `GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation `GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`) `RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test` diff --git a/doc/development/README.md b/doc/development/README.md index 12cca9f84b7..45e9565f9a7 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -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 diff --git a/doc/development/query_count_limits.md b/doc/development/query_count_limits.md new file mode 100644 index 00000000000..ebb6e0c2dac --- /dev/null +++ b/doc/development/query_count_limits.md @@ -0,0 +1,65 @@ +# 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 +``` diff --git a/doc/user/markdown.md b/doc/user/markdown.md index 552abac747b..b590dfa0d40 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -253,7 +253,7 @@ GFM will recognize the following: | `@user_name` | specific user | | `@group_name` | specific group | | `@all` | entire team | -| `#123` | issue | +| `#12345` | issue | | `!123` | merge request | | `$123` | snippet | | `~123` | label by ID | @@ -379,6 +379,45 @@ _Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._ >**Note:** This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual]. +### Colors + +> If this is not rendered correctly, see +https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#colors + +It is possible to have color written in HEX, RGB or HSL format rendered with a color indicator. + +Color written inside backticks will be followed by a color "chip". + +Examples: + + `#F00` + `#F00A` + `#FF0000` + `#FF0000AA` + `RGB(0,255,0)` + `RGB(0%,100%,0%)` + `RGBA(0,255,0,0.7)` + `HSL(540,70%,50%)` + `HSLA(540,70%,50%,0.7)` + +Becomes: + +`#F00` +`#F00A` +`#FF0000` +`#FF0000AA` +`RGB(0,255,0)` +`RGB(0%,100%,0%)` +`RGBA(0,255,0,0.7)` +`HSL(540,70%,50%)` +`HSLA(540,70%,50%,0.7)` + +#### Supported formats: + +* HEX: `` `#RGB[A]` `` or `` `#RRGGBB[AA]` `` +* RGB: `` `RGB[A](R, G, B[, A])` `` +* HSL: `` `HSL[A](H, S, L[, A])` `` + ### Mermaid > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15107) in diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index 72def9d1d1d..8c639bd5343 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -46,7 +46,7 @@ namespace that started the import process. The importer will also import branches on forks of projects related to open pull requests. These branches will be imported with a naming scheme similar to -GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepency +GH-SHA-Username/Pull-Request-number/fork-name/branch. This may lead to a discrepancy in branches compared to the GitHub Repository. For a more technical description and an overview of the architecture you can diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb index 9aeebc34525..c2113551207 100644 --- a/lib/api/api_guard.rb +++ b/lib/api/api_guard.rb @@ -42,7 +42,7 @@ module API include Gitlab::Auth::UserAuthFinders def find_current_user! - user = find_user_from_access_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 +50,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`) diff --git a/lib/api/branches.rb b/lib/api/branches.rb index 0791a110c39..1794207e29b 100644 --- a/lib/api/branches.rb +++ b/lib/api/branches.rb @@ -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)) diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb index 2cae53dba53..3d0d1287407 100644 --- a/lib/api/helpers/runner.rb +++ b/lib/api/helpers/runner.rb @@ -1,15 +1,13 @@ module API module Helpers module Runner - include Gitlab::CurrentSettings - JOB_TOKEN_HEADER = 'HTTP_JOB_TOKEN'.freeze JOB_TOKEN_PARAM = :token UPDATE_RUNNER_EVERY = 10 * 60 def runner_registration_token_valid? ActiveSupport::SecurityUtils.variable_size_secure_compare(params[:token], - current_application_settings.runners_registration_token) + Gitlab::CurrentSettings.runners_registration_token) end def get_runner_version_from_params @@ -70,7 +68,7 @@ module API end def max_artifacts_size - current_application_settings.max_artifacts_size.megabytes.to_i + Gitlab::CurrentSettings.max_artifacts_size.megabytes.to_i end end end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index c99fe3ab5b3..b6c278c89d0 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -161,6 +161,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 @@ -201,6 +203,8 @@ module API :labels, :created_at, :due_date, :confidential, :state_event 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 @@ -234,6 +238,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 diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 420aaf1c964..719afa09295 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -152,6 +152,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) @@ -256,6 +258,8 @@ module API at_least_one_of(*at_least_one_of_ce) 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) @@ -283,6 +287,8 @@ module API optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' 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]) diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb index 675c963bae2..d2b8b832e4e 100644 --- a/lib/api/pipelines.rb +++ b/lib/api/pipelines.rb @@ -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, diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 8b5e4f8edcc..5b481121a10 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -210,6 +210,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] diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb index dd6801664b1..b3709455bc3 100644 --- a/lib/api/triggers.rb +++ b/lib/api/triggers.rb @@ -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) } diff --git a/lib/api/users.rb b/lib/api/users.rb index e5de31ad51b..c7c2aa280d5 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -383,6 +383,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]) diff --git a/lib/api/v3/branches.rb b/lib/api/v3/branches.rb index b201bf77667..25176c5b38e 100644 --- a/lib/api/v3/branches.rb +++ b/lib/api/v3/branches.rb @@ -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)) diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb index cb371fdbab8..b59947d81d9 100644 --- a/lib/api/v3/issues.rb +++ b/lib/api/v3/issues.rb @@ -134,6 +134,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) @@ -169,6 +171,8 @@ module API :labels, :created_at, :due_date, :confidential, :state_event 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 @@ -201,6 +205,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 diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 0a24fea52a3..ce216497996 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -91,6 +91,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) @@ -167,6 +169,8 @@ module API :remove_source_branch 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) diff --git a/lib/api/v3/pipelines.rb b/lib/api/v3/pipelines.rb index c48cbd2b765..6d31c12f572 100644 --- a/lib/api/v3/pipelines.rb +++ b/lib/api/v3/pipelines.rb @@ -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 diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb index 534911fde5c..34f07dfb486 100644 --- a/lib/api/v3/triggers.rb +++ b/lib/api/v3/triggers.rb @@ -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) } diff --git a/lib/banzai/color_parser.rb b/lib/banzai/color_parser.rb new file mode 100644 index 00000000000..355c364b07b --- /dev/null +++ b/lib/banzai/color_parser.rb @@ -0,0 +1,44 @@ +module Banzai + module ColorParser + ALPHA = /0(?:\.\d+)?|\.\d+|1(?:\.0+)?/ # 0.0..1.0 + PERCENTS = /(?:\d{1,2}|100)%/ # 00%..100% + ALPHA_CHANNEL = /(?:,\s*(?:#{ALPHA}|#{PERCENTS}))?/ + BITS = /\d{1,2}|1\d\d|2(?:[0-4]\d|5[0-5])/ # 00..255 + DEGS = /-?\d+(?:deg)?/i # [-]digits[deg] + RADS = /-?(?:\d+(?:\.\d+)?|\.\d+)rad/i # [-](digits[.digits] OR .digits)rad + HEX_FORMAT = /\#(?:\h{3}|\h{4}|\h{6}|\h{8})/ + RGB_FORMAT = %r{ + (?:rgba? + \( + (?: + (?:(?:#{BITS},\s*){2}#{BITS}) + | + (?:(?:#{PERCENTS},\s*){2}#{PERCENTS}) + ) + #{ALPHA_CHANNEL} + \) + ) + }xi + HSL_FORMAT = %r{ + (?:hsla? + \( + (?:#{DEGS}|#{RADS}),\s*#{PERCENTS},\s*#{PERCENTS} + #{ALPHA_CHANNEL} + \) + ) + }xi + + FORMATS = [HEX_FORMAT, RGB_FORMAT, HSL_FORMAT].freeze + + COLOR_FORMAT = /\A(#{Regexp.union(FORMATS)})\z/ix + + # Public: Analyzes whether the String is a color code. + # + # text - The String to be parsed. + # + # Returns the recognized color String or nil if none was found. + def self.parse(text) + text if COLOR_FORMAT =~ text + end + end +end diff --git a/lib/banzai/filter/color_filter.rb b/lib/banzai/filter/color_filter.rb new file mode 100644 index 00000000000..6ab29ac281f --- /dev/null +++ b/lib/banzai/filter/color_filter.rb @@ -0,0 +1,31 @@ +module Banzai + module Filter + # HTML filter that renders `color` followed by a color "chip". + # + class ColorFilter < HTML::Pipeline::Filter + COLOR_CHIP_CLASS = 'gfm-color_chip'.freeze + + def call + doc.css('code').each do |node| + color = ColorParser.parse(node.content) + node << color_chip(color) if color + end + + doc + end + + private + + def color_chip(color) + checkerboard = doc.document.create_element('span', class: COLOR_CHIP_CLASS) + chip = doc.document.create_element('span', style: inline_styles(color: color)) + + checkerboard << chip + end + + def inline_styles(color:) + "background-color: #{color};" + end + end + end +end diff --git a/lib/banzai/pipeline/broadcast_message_pipeline.rb b/lib/banzai/pipeline/broadcast_message_pipeline.rb index adc09c8afbd..5dd572de3a1 100644 --- a/lib/banzai/pipeline/broadcast_message_pipeline.rb +++ b/lib/banzai/pipeline/broadcast_message_pipeline.rb @@ -7,6 +7,7 @@ module Banzai Filter::SanitizationFilter, Filter::EmojiFilter, + Filter::ColorFilter, Filter::AutolinkFilter, Filter::ExternalLinkFilter ] diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index c746f6f64e9..4001b8a85e3 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -14,6 +14,7 @@ module Banzai Filter::SyntaxHighlightFilter, Filter::MathFilter, + Filter::ColorFilter, Filter::MermaidFilter, Filter::VideoLinkFilter, Filter::ImageLazyLoadFilter, diff --git a/lib/email_template_interceptor.rb b/lib/email_template_interceptor.rb index f2bf3d0fb2b..3978a6d9fe4 100644 --- a/lib/email_template_interceptor.rb +++ b/lib/email_template_interceptor.rb @@ -1,10 +1,8 @@ # Read about interceptors in http://guides.rubyonrails.org/action_mailer_basics.html#intercepting-emails class EmailTemplateInterceptor - extend Gitlab::CurrentSettings - def self.delivering_email(message) # Remove HTML part if HTML emails are disabled. - unless current_application_settings.html_emails_enabled + unless Gitlab::CurrentSettings.html_emails_enabled message.parts.delete_if do |part| part.content_type.start_with?('text/html') end diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb index cead1c7eacd..ee7f4be6b9f 100644 --- a/lib/gitlab/asciidoc.rb +++ b/lib/gitlab/asciidoc.rb @@ -6,8 +6,6 @@ module Gitlab # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters # the resulting HTML through HTML pipeline filters. module Asciidoc - extend Gitlab::CurrentSettings - DEFAULT_ADOC_ATTRS = [ 'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab', 'env-gitlab', 'source-highlighter=html-pipeline', 'icons=font' @@ -33,9 +31,9 @@ module Gitlab def self.plantuml_setup Asciidoctor::PlantUml.configure do |conf| - conf.url = current_application_settings.plantuml_url - conf.svg_enable = current_application_settings.plantuml_enabled - conf.png_enable = current_application_settings.plantuml_enabled + conf.url = Gitlab::CurrentSettings.plantuml_url + conf.svg_enable = Gitlab::CurrentSettings.plantuml_enabled + conf.png_enable = Gitlab::CurrentSettings.plantuml_enabled conf.txt_enable = false end end diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 65d7fd3ec70..05932378173 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -14,8 +14,6 @@ module Gitlab DEFAULT_SCOPES = [:api].freeze class << self - include Gitlab::CurrentSettings - def find_for_git_client(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? @@ -57,7 +55,7 @@ module Gitlab if user.nil? || user.ldap_user? # Second chance - try LDAP authentication Gitlab::LDAP::Authentication.login(login, password) - elsif current_application_settings.password_authentication_enabled_for_git? + elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git? user if user.active? && user.valid_password?(password) end end @@ -87,7 +85,7 @@ module Gitlab private def authenticate_using_internal_or_ldap_password? - current_application_settings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled? + Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled? end def service_request_check(login, password, project) diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb index 9a0482306b7..778d78185ff 100644 --- a/lib/gitlab/badge/coverage/report.rb +++ b/lib/gitlab/badge/coverage/report.rb @@ -23,7 +23,7 @@ module Gitlab @coverage ||= raw_coverage return unless @coverage - @coverage.to_i + @coverage.to_f.round(2) end def metadata diff --git a/lib/gitlab/badge/coverage/template.rb b/lib/gitlab/badge/coverage/template.rb index fcecb1d9665..afbf9dd17e3 100644 --- a/lib/gitlab/badge/coverage/template.rb +++ b/lib/gitlab/badge/coverage/template.rb @@ -25,7 +25,7 @@ module Gitlab end def value_text - @status ? "#{@status}%" : 'unknown' + @status ? ("%.2f%%" % @status) : 'unknown' end def key_width @@ -33,7 +33,7 @@ module Gitlab end def value_width - @status ? 36 : 58 + @status ? 54 : 58 end def value_color diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 91fd9cc7631..b7c596a973d 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -1,73 +1,79 @@ module Gitlab module CurrentSettings - extend self + class << self + def current_application_settings + if RequestStore.active? + RequestStore.fetch(:current_application_settings) { ensure_application_settings! } + else + ensure_application_settings! + end + end - def current_application_settings - if RequestStore.active? - RequestStore.fetch(:current_application_settings) { ensure_application_settings! } - else - ensure_application_settings! + def fake_application_settings(defaults = ::ApplicationSetting.defaults) + Gitlab::FakeApplicationSettings.new(defaults) end - end - delegate :sidekiq_throttling_enabled?, to: :current_application_settings + def method_missing(name, *args, &block) + current_application_settings.send(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend + end - def fake_application_settings(defaults = ::ApplicationSetting.defaults) - FakeApplicationSettings.new(defaults) - end + def respond_to_missing?(name, include_private = false) + current_application_settings.respond_to?(name, include_private) || super + end - private + private - def ensure_application_settings! - return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' + def ensure_application_settings! + return in_memory_application_settings if ENV['IN_MEMORY_APPLICATION_SETTINGS'] == 'true' - cached_application_settings || uncached_application_settings - end + cached_application_settings || uncached_application_settings + end - def cached_application_settings - begin - ::ApplicationSetting.cached - rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL - # In case Redis isn't running or the Redis UNIX socket file is not available + def cached_application_settings + begin + ::ApplicationSetting.cached + rescue ::Redis::BaseError, ::Errno::ENOENT, ::Errno::EADDRNOTAVAIL + # In case Redis isn't running or the Redis UNIX socket file is not available + end end - end - def uncached_application_settings - return fake_application_settings unless connect_to_db? + def uncached_application_settings + return fake_application_settings unless connect_to_db? - db_settings = ::ApplicationSetting.current + db_settings = ::ApplicationSetting.current - # If there are pending migrations, it's possible there are columns that - # need to be added to the application settings. To prevent Rake tasks - # and other callers from failing, use any loaded settings and return - # defaults for missing columns. - if ActiveRecord::Migrator.needs_migration? - defaults = ::ApplicationSetting.defaults - defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present? - return fake_application_settings(defaults) - end + # If there are pending migrations, it's possible there are columns that + # need to be added to the application settings. To prevent Rake tasks + # and other callers from failing, use any loaded settings and return + # defaults for missing columns. + if ActiveRecord::Migrator.needs_migration? + defaults = ::ApplicationSetting.defaults + defaults.merge!(db_settings.attributes.symbolize_keys) if db_settings.present? + return fake_application_settings(defaults) + end - return db_settings if db_settings.present? + return db_settings if db_settings.present? - ::ApplicationSetting.create_from_defaults || in_memory_application_settings - end + ::ApplicationSetting.create_from_defaults || in_memory_application_settings + end - def in_memory_application_settings - @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables - rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError - # In case migrations the application_settings table is not created yet, - # we fallback to a simple OpenStruct - fake_application_settings - end + def in_memory_application_settings + @in_memory_application_settings ||= ::ApplicationSetting.new(::ApplicationSetting.defaults) # rubocop:disable Gitlab/ModuleWithInstanceVariables + rescue ActiveRecord::StatementInvalid, ActiveRecord::UnknownAttributeError + # In case migrations the application_settings table is not created yet, + # we fallback to a simple OpenStruct + fake_application_settings + end - def connect_to_db? - # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised - active_db_connection = ActiveRecord::Base.connection.active? rescue false + def connect_to_db? + # When the DBMS is not available, an exception (e.g. PG::ConnectionBad) is raised + active_db_connection = ActiveRecord::Base.connection.active? rescue false - active_db_connection && - ActiveRecord::Base.connection.table_exists?('application_settings') - rescue ActiveRecord::NoDatabaseError - false + active_db_connection && + ActiveRecord::Base.connection.table_exists?('application_settings') + rescue ActiveRecord::NoDatabaseError + false + end end end end diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb index 3487e099381..ae7e88f0503 100644 --- a/lib/gitlab/git/branch.rb +++ b/lib/gitlab/git/branch.rb @@ -1,5 +1,3 @@ -# Gitaly note: JV: no RPC's here. - module Gitlab module Git class Branch < Ref diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index f28624ff37a..ab1362a3bb0 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -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 @@ -1363,20 +1343,23 @@ module Gitlab raise CommandError.new(e) end - def refs_contains_sha(ref_type, sha) - args = %W(#{ref_type} --contains #{sha}) - names = run_git(args).first - - if names.respond_to?(:split) - names = names.split("\n").map(&:strip) - - names.each do |name| - name.slice! '* ' + def branch_names_contains_sha(sha) + gitaly_migrate(:branch_names_contains_sha) do |is_enabled| + if is_enabled + gitaly_ref_client.branch_names_contains_sha(sha) + else + refs_contains_sha(:branch, sha) end + end + end - names - else - [] + def tag_names_contains_sha(sha) + gitaly_migrate(:tag_names_contains_sha) do |is_enabled| + if is_enabled + gitaly_ref_client.tag_names_contains_sha(sha) + else + refs_contains_sha(:tag, sha) + end end end @@ -1454,6 +1437,21 @@ module Gitlab end end + def refs_contains_sha(ref_type, sha) + args = %W(#{ref_type} --contains #{sha}) + names = run_git(args).first + + return [] unless names.respond_to?(:split) + + names = names.split("\n").map(&:strip) + + names.each do |name| + name.slice! '* ' + end + + names + end + def rugged_write_config(full_path:) rugged.config['gitlab.fullpath'] = full_path end @@ -2164,6 +2162,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) diff --git a/lib/gitlab/git/tag.rb b/lib/gitlab/git/tag.rb index bc4e160dce9..8a8f7b051ed 100644 --- a/lib/gitlab/git/tag.rb +++ b/lib/gitlab/git/tag.rb @@ -1,5 +1,3 @@ -# Gitaly note: JV: no RPC's here. -# module Gitlab module Git class Tag < Ref diff --git a/lib/gitlab/gitaly_client/operation_service.rb b/lib/gitlab/gitaly_client/operation_service.rb index cd2734b5a07..831cfd1e014 100644 --- a/lib/gitlab/gitaly_client/operation_service.rb +++ b/lib/gitlab/gitaly_client/operation_service.rb @@ -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) diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb index 8b9a224b700..ba6b577fd17 100644 --- a/lib/gitlab/gitaly_client/ref_service.rb +++ b/lib/gitlab/gitaly_client/ref_service.rb @@ -145,6 +145,32 @@ module Gitlab raise Gitlab::Git::Repository::GitError, response.git_error if response.git_error.present? end + # Limit: 0 implies no limit, thus all tag names will be returned + def tag_names_contains_sha(sha, limit: 0) + request = Gitaly::ListTagNamesContainingCommitRequest.new( + repository: @gitaly_repo, + commit_id: sha, + limit: limit + ) + + stream = GitalyClient.call(@repository.storage, :ref_service, :list_tag_names_containing_commit, request) + + consume_ref_contains_sha_response(stream, :tag_names) + end + + # Limit: 0 implies no limit, thus all tag names will be returned + def branch_names_contains_sha(sha, limit: 0) + request = Gitaly::ListBranchNamesContainingCommitRequest.new( + repository: @gitaly_repo, + commit_id: sha, + limit: limit + ) + + stream = GitalyClient.call(@repository.storage, :ref_service, :list_branch_names_containing_commit, request) + + consume_ref_contains_sha_response(stream, :branch_names) + end + private def consume_refs_response(response) @@ -215,6 +241,13 @@ module Gitlab Gitlab::Git::Commit.decorate(@repository, hash) end + def consume_ref_contains_sha_response(stream, collection_name) + stream.each_with_object([]) do |response, array| + encoded_names = response.send(collection_name).map { |b| Gitlab::Git.ref_name(b) } # rubocop:disable GitlabSecurity/PublicSend + array.concat(encoded_names) + end + end + def invalid_ref!(message) raise Gitlab::Git::Repository::InvalidRef.new(message) end diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 9148d7571f2..86a90d57d9c 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -3,12 +3,11 @@ module Gitlab module GonHelper include WebpackHelper - include Gitlab::CurrentSettings def add_gon_variables gon.api_version = 'v4' gon.default_avatar_url = URI.join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s - gon.max_file_size = current_application_settings.max_attachment_size + gon.max_file_size = Gitlab::CurrentSettings.max_attachment_size gon.asset_host = ActionController::Base.asset_host gon.webpack_public_path = webpack_public_path gon.relative_url_root = Gitlab.config.gitlab.relative_url_root @@ -16,7 +15,7 @@ module Gitlab gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') - gon.sentry_dsn = current_application_settings.clientside_sentry_dsn if current_application_settings.clientside_sentry_enabled + gon.sentry_dsn = Gitlab::CurrentSettings.clientside_sentry_dsn if Gitlab::CurrentSettings.clientside_sentry_enabled gon.gitlab_url = Gitlab.config.gitlab.url gon.revision = Gitlab::REVISION gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png') diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index cb711a83433..759833a5ee5 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -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 diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb index a3216759cae..ca5e06009fa 100644 --- a/lib/gitlab/kubernetes/helm/pod.rb +++ b/lib/gitlab/kubernetes/helm/pod.rb @@ -64,7 +64,7 @@ module Gitlab { name: 'configuration-volume', configMap: { - name: 'values-content-configuration', + name: "values-content-configuration-#{command.name}", items: [{ key: 'values', path: 'values.yaml' }] } } @@ -81,7 +81,11 @@ module Gitlab def create_config_map resource = ::Kubeclient::Resource.new - resource.metadata = { name: 'values-content-configuration', namespace: namespace_name, labels: { name: 'values-content-configuration' } } + resource.metadata = { + name: "values-content-configuration-#{command.name}", + namespace: namespace_name, + labels: { name: "values-content-configuration-#{command.name}" } + } resource.data = { values: File.read(command.chart_values_file) } kubeclient.create_config_map(resource) end diff --git a/lib/gitlab/legacy_github_import/project_creator.rb b/lib/gitlab/legacy_github_import/project_creator.rb index 41e7eac4d08..cbabe5454ca 100644 --- a/lib/gitlab/legacy_github_import/project_creator.rb +++ b/lib/gitlab/legacy_github_import/project_creator.rb @@ -1,8 +1,6 @@ module Gitlab module LegacyGithubImport class ProjectCreator - include Gitlab::CurrentSettings - attr_reader :repo, :name, :namespace, :current_user, :session_data, :type def initialize(repo, name, namespace, current_user, session_data, type: 'github') @@ -36,7 +34,7 @@ module Gitlab end def visibility_level - repo.private ? Gitlab::VisibilityLevel::PRIVATE : current_application_settings.default_project_visibility + repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::CurrentSettings.default_project_visibility end # diff --git a/lib/gitlab/metrics/prometheus.rb b/lib/gitlab/metrics/prometheus.rb index f07ea3560ff..d12ba0ec176 100644 --- a/lib/gitlab/metrics/prometheus.rb +++ b/lib/gitlab/metrics/prometheus.rb @@ -71,8 +71,7 @@ module Gitlab end def prometheus_metrics_enabled_unmemoized - metrics_folder_present? && - Gitlab::CurrentSettings.current_application_settings[:prometheus_metrics_enabled] || false + metrics_folder_present? && Gitlab::CurrentSettings.prometheus_metrics_enabled || false end end end diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb index afbc2600634..1a570f480c6 100644 --- a/lib/gitlab/middleware/go.rb +++ b/lib/gitlab/middleware/go.rb @@ -4,7 +4,6 @@ module Gitlab module Middleware class Go include ActionView::Helpers::TagHelper - include Gitlab::CurrentSettings PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze @@ -42,7 +41,7 @@ module Gitlab project_url = URI.join(config.gitlab.url, path) import_prefix = strip_url(project_url.to_s) - repository_url = if current_application_settings.enabled_git_access_protocol == 'ssh' + repository_url = if Gitlab::CurrentSettings.enabled_git_access_protocol == 'ssh' shell = config.gitlab_shell port = ":#{shell.ssh_port}" unless shell.ssh_port == 22 "ssh://#{shell.ssh_user}@#{shell.ssh_host}#{port}/#{path}.git" diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb index e29e168fc5a..6c2b2036074 100644 --- a/lib/gitlab/performance_bar.rb +++ b/lib/gitlab/performance_bar.rb @@ -1,7 +1,5 @@ module Gitlab module PerformanceBar - extend Gitlab::CurrentSettings - ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids:v2'.freeze EXPIRY_TIME = 5.minutes @@ -13,7 +11,7 @@ module Gitlab end def self.allowed_group_id - current_application_settings.performance_bar_allowed_group_id + Gitlab::CurrentSettings.performance_bar_allowed_group_id end def self.allowed_user_ids diff --git a/lib/gitlab/polling_interval.rb b/lib/gitlab/polling_interval.rb index 4780675a492..fe4bdfe3831 100644 --- a/lib/gitlab/polling_interval.rb +++ b/lib/gitlab/polling_interval.rb @@ -1,12 +1,10 @@ module Gitlab class PollingInterval - extend Gitlab::CurrentSettings - HEADER_NAME = 'Poll-Interval'.freeze def self.set_header(response, interval:) if polling_enabled? - multiplier = current_application_settings.polling_interval_multiplier + multiplier = Gitlab::CurrentSettings.polling_interval_multiplier value = (interval * multiplier).to_i else value = -1 @@ -16,7 +14,7 @@ module Gitlab end def self.polling_enabled? - !current_application_settings.polling_interval_multiplier.zero? + !Gitlab::CurrentSettings.polling_interval_multiplier.zero? end end end diff --git a/lib/gitlab/protocol_access.rb b/lib/gitlab/protocol_access.rb index 09fa14764e6..2819c7d062c 100644 --- a/lib/gitlab/protocol_access.rb +++ b/lib/gitlab/protocol_access.rb @@ -1,14 +1,12 @@ module Gitlab module ProtocolAccess - extend Gitlab::CurrentSettings - def self.allowed?(protocol) if protocol == 'web' true - elsif current_application_settings.enabled_git_access_protocol.blank? + elsif Gitlab::CurrentSettings.enabled_git_access_protocol.blank? true else - protocol == current_application_settings.enabled_git_access_protocol + protocol == Gitlab::CurrentSettings.enabled_git_access_protocol end end end diff --git a/lib/gitlab/query_limiting.rb b/lib/gitlab/query_limiting.rb new file mode 100644 index 00000000000..f64f1757144 --- /dev/null +++ b/lib/gitlab/query_limiting.rb @@ -0,0 +1,36 @@ +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 diff --git a/lib/gitlab/query_limiting/active_support_subscriber.rb b/lib/gitlab/query_limiting/active_support_subscriber.rb new file mode 100644 index 00000000000..66049c94ec6 --- /dev/null +++ b/lib/gitlab/query_limiting/active_support_subscriber.rb @@ -0,0 +1,11 @@ +module Gitlab + module QueryLimiting + class ActiveSupportSubscriber < ActiveSupport::Subscriber + attach_to :active_record + + def sql(*) + Transaction.current&.increment + end + end + end +end diff --git a/lib/gitlab/query_limiting/middleware.rb b/lib/gitlab/query_limiting/middleware.rb new file mode 100644 index 00000000000..949ae79a047 --- /dev/null +++ b/lib/gitlab/query_limiting/middleware.rb @@ -0,0 +1,55 @@ +# 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 diff --git a/lib/gitlab/query_limiting/transaction.rb b/lib/gitlab/query_limiting/transaction.rb new file mode 100644 index 00000000000..3cbafadb0d0 --- /dev/null +++ b/lib/gitlab/query_limiting/transaction.rb @@ -0,0 +1,83 @@ +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 diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb index c463dd487a0..c9efa28d7e7 100644 --- a/lib/gitlab/recaptcha.rb +++ b/lib/gitlab/recaptcha.rb @@ -1,12 +1,10 @@ module Gitlab module Recaptcha - extend Gitlab::CurrentSettings - def self.load_configurations! - if current_application_settings.recaptcha_enabled + if Gitlab::CurrentSettings.recaptcha_enabled ::Recaptcha.configure do |config| - config.public_key = current_application_settings.recaptcha_site_key - config.private_key = current_application_settings.recaptcha_private_key + config.public_key = Gitlab::CurrentSettings.recaptcha_site_key + config.private_key = Gitlab::CurrentSettings.recaptcha_private_key end true @@ -14,7 +12,7 @@ module Gitlab end def self.enabled? - current_application_settings.recaptcha_enabled + Gitlab::CurrentSettings.recaptcha_enabled end end end diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb index 159d0e7952e..4a22fc80f75 100644 --- a/lib/gitlab/sentry.rb +++ b/lib/gitlab/sentry.rb @@ -1,9 +1,7 @@ module Gitlab module Sentry - extend Gitlab::CurrentSettings - def self.enabled? - Rails.env.production? && current_application_settings.sentry_enabled? + Rails.env.production? && Gitlab::CurrentSettings.sentry_enabled? end def self.context(current_user = nil) diff --git a/lib/gitlab/ssh_public_key.rb b/lib/gitlab/ssh_public_key.rb index 89ca1298120..545e7c74f7e 100644 --- a/lib/gitlab/ssh_public_key.rb +++ b/lib/gitlab/ssh_public_key.rb @@ -21,6 +21,22 @@ module Gitlab technology(name)&.supported_sizes end + def self.sanitize(key_content) + ssh_type, *parts = key_content.strip.split + + return key_content if parts.empty? + + parts.each_with_object("#{ssh_type} ").with_index do |(part, content), index| + content << part + + if Gitlab::SSHPublicKey.new(content).valid? + break [content, parts[index + 1]].compact.join(' ') # Add the comment part if present + elsif parts.size == index + 1 # return original content if we've reached the last element + break key_content + end + end + end + attr_reader :key_text, :key # Unqualified MD5 fingerprint for compatibility @@ -37,23 +53,23 @@ module Gitlab end def valid? - key.present? + key.present? && bits && technology.supported_sizes.include?(bits) end def type - technology.name if valid? + technology.name if key.present? end def bits - return unless valid? + return if key.blank? case type when :rsa - key.n.num_bits + key.n&.num_bits when :dsa - key.p.num_bits + key.p&.num_bits when :ecdsa - key.group.order.num_bits + key.group.order&.num_bits when :ed25519 256 else diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 2adcc9809b3..9d13d1d781f 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -1,8 +1,6 @@ module Gitlab class UsageData class << self - include Gitlab::CurrentSettings - def data(force_refresh: false) Rails.cache.fetch('usage_data', force: force_refresh, expires_in: 2.weeks) { uncached_data } end @@ -19,7 +17,7 @@ module Gitlab def license_usage_data usage_data = { - uuid: current_application_settings.uuid, + uuid: Gitlab::CurrentSettings.uuid, hostname: Gitlab.config.gitlab.host, version: Gitlab::VERSION, active_user_count: User.active.count, @@ -79,9 +77,9 @@ module Gitlab def features_usage_data_ce { - signup: current_application_settings.allow_signup?, + signup: Gitlab::CurrentSettings.allow_signup?, ldap: Gitlab.config.ldap.enabled, - gravatar: current_application_settings.gravatar_enabled?, + gravatar: Gitlab::CurrentSettings.gravatar_enabled?, omniauth: Gitlab.config.omniauth.enabled, reply_by_email: Gitlab::IncomingEmail.enabled?, container_registry: Gitlab.config.registry.enabled, diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index 6ced06a863d..2612208a927 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -5,7 +5,6 @@ # module Gitlab module VisibilityLevel - extend CurrentSettings extend ActiveSupport::Concern included do @@ -58,9 +57,9 @@ module Gitlab end def allowed_levels - restricted_levels = current_application_settings.restricted_visibility_levels + restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels - self.values - restricted_levels + self.values - Array(restricted_levels) end def closest_allowed_level(target_level) @@ -81,7 +80,7 @@ module Gitlab end def non_restricted_level?(level) - restricted_levels = current_application_settings.restricted_visibility_levels + restricted_levels = Gitlab::CurrentSettings.restricted_visibility_levels if restricted_levels.nil? true diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake index b1e012e70c5..4b4881cecb8 100644 --- a/lib/tasks/flay.rake +++ b/lib/tasks/flay.rake @@ -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 diff --git a/package.json b/package.json index c68cf648932..26908e9f905 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "scripts": { - "dev-server": "nodemon --watch config/webpack.config.js -- ./node_modules/.bin/webpack-dev-server --config config/webpack.config.js", + "dev-server": "nodemon -w 'config/webpack.config.js' -w 'app/assets/javascripts/dispatcher.js' -w 'app/assets/javascripts/pages/**/index.js' --exec 'webpack-dev-server --config config/webpack.config.js'", "eslint": "eslint --max-warnings 0 --ext .js,.vue .", "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .", @@ -47,6 +47,7 @@ "exports-loader": "^0.6.4", "file-loader": "^0.11.1", "fuzzaldrin-plus": "^0.5.0", + "glob": "^7.1.2", "imports-loader": "^0.7.1", "jed": "^1.1.1", "jquery": "^2.2.4", diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb index 2cead1770c9..387ca46ef6f 100644 --- a/spec/controllers/health_check_controller_spec.rb +++ b/spec/controllers/health_check_controller_spec.rb @@ -5,7 +5,7 @@ describe HealthCheckController do let(:json_response) { JSON.parse(response.body) } let(:xml_response) { Hash.from_xml(response.body)['hash'] } - let(:token) { current_application_settings.health_check_access_token } + let(:token) { Gitlab::CurrentSettings.health_check_access_token } let(:whitelisted_ip) { '127.0.0.1' } let(:not_whitelisted_ip) { '127.0.0.2' } diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb index 95946def5f9..542eddc2d16 100644 --- a/spec/controllers/health_controller_spec.rb +++ b/spec/controllers/health_controller_spec.rb @@ -4,7 +4,7 @@ describe HealthController do include StubENV let(:json_response) { JSON.parse(response.body) } - let(:token) { current_application_settings.health_check_access_token } + let(:token) { Gitlab::CurrentSettings.health_check_access_token } let(:whitelisted_ip) { '127.0.0.1' } let(:not_whitelisted_ip) { '127.0.0.2' } diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index b38652e7ab9..1195f44f37d 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -16,8 +16,7 @@ describe Oauth::ApplicationsController do end it 'redirects back to profile page if OAuth applications are disabled' do - settings = double(user_oauth_applications?: false) - allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings) + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false) get :index diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb new file mode 100644 index 00000000000..48e2ff75cac --- /dev/null +++ b/spec/controllers/user_callouts_controller_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe UserCalloutsController do + let(:user) { create(:user) } + + before do + sign_in(user) + end + + describe "POST #create" do + subject { post :create, feature_name: feature_name, format: :json } + + context 'with valid feature name' do + let(:feature_name) { UserCallout.feature_names.keys.first } + + context 'when callout entry does not exist' do + it 'should create a callout entry with dismissed state' do + expect { subject }.to change { UserCallout.count }.by(1) + end + + it 'should return success' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when callout entry already exists' do + let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) } + + it 'should return success' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with invalid feature name' do + let(:feature_name) { 'bogus_feature_name' } + + it 'should return bad request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index f0c43f3d6f5..23a98a899f1 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -5,6 +5,10 @@ FactoryBot.define do title key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' } + factory :key_without_comment do + key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate } + end + factory :deploy_key, class: 'DeployKey' factory :personal_key do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 16d328a5bc2..20976977f21 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -93,6 +93,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') diff --git a/spec/factories/user_callouts.rb b/spec/factories/user_callouts.rb new file mode 100644 index 00000000000..528e442c14b --- /dev/null +++ b/spec/factories/user_callouts.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :user_callout do + feature_name :gke_cluster_integration + + user + end +end diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index ac3392b49f9..3693e5882f9 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -17,7 +17,7 @@ feature "Admin Health Check", :feature do page.has_text? 'Health Check' page.has_text? 'Health information can be retrieved' - token = current_application_settings.health_check_access_token + token = Gitlab::CurrentSettings.health_check_access_token expect(page).to have_content("Access token is #{token}") expect(page).to have_selector('#health-check-token', text: token) @@ -25,7 +25,7 @@ feature "Admin Health Check", :feature do describe 'reload access token' do it 'changes the access token' do - orig_token = current_application_settings.health_check_access_token + orig_token = Gitlab::CurrentSettings.health_check_access_token click_button 'Reset health check access token' expect(page).to have_content('New health check access token has been generated!') diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index c1c54177167..a01c129defd 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -156,7 +156,7 @@ describe "Admin Runners" do end describe 'runners registration token' do - let!(:token) { current_application_settings.runners_registration_token } + let!(:token) { Gitlab::CurrentSettings.runners_registration_token } before do visit admin_runners_path diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 1218ea52227..cc0849d1cc6 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -38,12 +38,12 @@ feature 'Admin updates settings' do uncheck 'Project export enabled' click_button 'Save' - expect(current_application_settings.gravatar_enabled).to be_falsey - expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/" - expect(current_application_settings.help_page_text).to eq "Example text" - expect(current_application_settings.help_page_hide_commercial_content).to be_truthy - expect(current_application_settings.help_page_support_url).to eq "http://example.com/help" - expect(current_application_settings.project_export_enabled).to be_falsey + expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey + expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/" + expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text" + expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy + expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help" + expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey expect(page).to have_content "Application settings saved successfully" end diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index c7cfd01f588..a75ca1d42b3 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -9,7 +9,7 @@ describe 'New issue', :js do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - current_application_settings.update!( + Gitlab::CurrentSettings.update!( akismet_enabled: true, akismet_api_key: 'testkey', recaptcha_enabled: true, diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index a2b78a5e021..f13d78d24e3 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -259,6 +259,10 @@ describe 'GitLab Markdown' do it 'includes VideoLinkFilter' do expect(doc).to parse_video_links end + + it 'includes ColorFilter' do + expect(doc).to parse_colors + end end context 'wiki pipeline' do @@ -320,6 +324,10 @@ describe 'GitLab Markdown' do it 'includes VideoLinkFilter' do expect(doc).to parse_video_links end + + it 'includes ColorFilter' do + expect(doc).to parse_colors + end end # Fake a `current_user` helper diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb index 821ce88a402..f51001edcd7 100644 --- a/spec/features/projects/badges/coverage_spec.rb +++ b/spec/features/projects/badges/coverage_spec.rb @@ -18,7 +18,7 @@ feature 'test coverage badge' do show_test_coverage_badge - expect_coverage_badge('95%') + expect_coverage_badge('95.00%') end scenario 'user requests coverage badge for specific job' do @@ -30,7 +30,7 @@ feature 'test coverage badge' do show_test_coverage_badge(job: 'coverage') - expect_coverage_badge('85%') + expect_coverage_badge('85.00%') end scenario 'user requests coverage badge for pipeline without coverage' do diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index e76bc6f1220..c1fccf4a40b 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -1,44 +1,37 @@ 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 diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb index fa2f7a1fd78..65e24862d43 100644 --- a/spec/features/projects/pipeline_schedules_spec.rb +++ b/spec/features/projects/pipeline_schedules_spec.rb @@ -168,11 +168,11 @@ feature 'Pipeline Schedules', :js do scenario 'user sees the new variable in edit window' do find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - page.within('.pipeline-variable-list') do - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('AAA') - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('AAA123') - expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-key-input").value).to eq('BBB') - expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-value-input").value).to eq('BBB123') + page.within('.ci-variable-list') do + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA') + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123') + expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB') + expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123') end end end @@ -185,16 +185,18 @@ feature 'Pipeline Schedules', :js do visit_pipelines_schedules find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - all('[name="schedule[variables_attributes][][key]"]')[0].set('foo') - all('[name="schedule[variables_attributes][][value]"]')[0].set('bar') + + find('.js-ci-variable-list-section .js-secret-value-reveal-button').click + first('.js-ci-variable-input-key').set('foo') + first('.js-ci-variable-input-value').set('bar') click_button 'Save pipeline schedule' end scenario 'user sees the updated variable in edit window' do find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - page.within('.pipeline-variable-list') do - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('foo') - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('bar') + page.within('.ci-variable-list') do + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo') + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar') end end end @@ -207,15 +209,15 @@ feature 'Pipeline Schedules', :js do visit_pipelines_schedules find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - find('.pipeline-variable-list .pipeline-variable-row-remove-button').click + find('.ci-variable-list .ci-variable-row-remove-button').click click_button 'Save pipeline schedule' end scenario 'user does not see the removed variable in edit window' do find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click - page.within('.pipeline-variable-list') do - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('') - expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('') + page.within('.ci-variable-list') do + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('') + expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('') end end end diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 71abb6da607..da32a46675f 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such: ![My Video](/assets/videos/gitlab-demo.mp4) +### Colors + +`#F00` +`#F00A` +`#FF0000` +`#FF0000AA` +`RGB(0,255,0)` +`RGB(0%,100%,0%)` +`RGBA(0,255,0,0.7)` +`HSL(540,70%,50%)` +`HSLA(540,70%,50%,0.7)` + ### Mermaid > If this is not rendered correctly, see diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb new file mode 100644 index 00000000000..27455705d23 --- /dev/null +++ b/spec/helpers/user_callouts_helper_spec.rb @@ -0,0 +1,47 @@ +require "spec_helper" + +describe UserCalloutsHelper do + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + end + + describe '.show_gke_cluster_integration_callout?' do + let(:project) { create(:project) } + + subject { helper.show_gke_cluster_integration_callout?(project) } + + context 'when user can create a cluster' do + before do + allow(helper).to receive(:can?).with(anything, :create_cluster, anything) + .and_return(true) + end + + context 'when user has not dismissed' do + before do + allow(helper).to receive(:user_dismissed?).and_return(false) + end + + it { is_expected.to be true } + end + + context 'when user dismissed' do + before do + allow(helper).to receive(:user_dismissed?).and_return(true) + end + + it { is_expected.to be false } + end + end + + context 'when user can not create a cluster' do + before do + allow(helper).to receive(:can?).with(anything, :create_cluster, anything) + .and_return(false) + end + + it { is_expected.to be false } + end + end +end diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb index b5b15726816..9d4e34abef5 100644 --- a/spec/helpers/version_check_helper_spec.rb +++ b/spec/helpers/version_check_helper_spec.rb @@ -4,7 +4,7 @@ describe VersionCheckHelper do describe '#version_status_badge' do it 'should return nil if not dev environment and not enabled' do allow(Rails.env).to receive(:production?) { false } - allow(helper.current_application_settings).to receive(:version_check_enabled) { false } + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false } expect(helper.version_status_badge).to be(nil) end @@ -12,7 +12,7 @@ describe VersionCheckHelper do context 'when production and enabled' do before do allow(Rails.env).to receive(:production?) { true } - allow(helper.current_application_settings).to receive(:version_check_enabled) { true } + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true } allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' } @image_tag = helper.version_status_badge diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js new file mode 100644 index 00000000000..0170ab458d4 --- /dev/null +++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js @@ -0,0 +1,163 @@ +import VariableList from '~/ci_variable_list/ci_variable_list'; +import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper'; + +describe('VariableList', () => { + preloadFixtures('pipeline_schedules/edit.html.raw'); + preloadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + + let $wrapper; + let variableList; + + describe('with only key/value inputs', () => { + describe('with no variables', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'schedule', + }); + variableList.init(); + }); + + it('should remove the row when clicking the remove button', () => { + $wrapper.find('.js-row-remove-button').trigger('click'); + + expect($wrapper.find('.js-row').length).toBe(0); + }); + + it('should add another row when editing the last rows key input', () => { + const $row = $wrapper.find('.js-row'); + $row.find('.js-ci-variable-input-key') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key'); + expect($keyInput.val()).toBe(''); + }); + + it('should add another row when editing the last rows value textarea', () => { + const $row = $wrapper.find('.js-row'); + $row.find('.js-ci-variable-input-value') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key'); + expect($valueInput.val()).toBe(''); + }); + + it('should remove empty row after blurring', () => { + const $row = $wrapper.find('.js-row'); + $row.find('.js-ci-variable-input-key') + .val('foo') + .trigger('input'); + + expect($wrapper.find('.js-row').length).toBe(2); + + $row.find('.js-ci-variable-input-key') + .val('') + .trigger('input') + .trigger('blur'); + + expect($wrapper.find('.js-row').length).toBe(1); + }); + }); + + describe('with persisted variables', () => { + beforeEach(() => { + loadFixtures('pipeline_schedules/edit_with_variables.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + variableList = new VariableList({ + container: $wrapper, + formField: 'schedule', + }); + variableList.init(); + }); + + it('should have "Reveal values" button initially when there are already variables', () => { + expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values'); + }); + + it('should reveal hidden values', () => { + const $row = $wrapper.find('.js-row:first-child'); + const $inputValue = $row.find('.js-ci-variable-input-value'); + const $placeholder = $row.find('.js-secret-value-placeholder'); + + expect($placeholder.hasClass('hide')).toBe(false); + expect($inputValue.hasClass('hide')).toBe(true); + + // Reveal values + $wrapper.find('.js-secret-value-reveal-button').click(); + + expect($placeholder.hasClass('hide')).toBe(true); + expect($inputValue.hasClass('hide')).toBe(false); + }); + }); + }); + + describe('with all inputs(key, value, protected)', () => { + beforeEach(() => { + // This markup will be replaced with a fixture when we can render the + // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110 + $wrapper = $(`<form class="js-variable-list"> + <ul> + <li class="js-row"> + <div class="ci-variable-body-item"> + <input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]"> + </div> + + <div class="ci-variable-body-item"> + <textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea> + </div> + + <div class="ci-variable-body-item ci-variable-protected-item"> + <button type="button" class="js-project-feature-toggle project-feature-toggle"> + <input + type="hidden" + class="js-ci-variable-input-protected js-project-feature-toggle-input" + name="variables[variables_attributes][][protected]" + value="true" + /> + </button> + </div> + + <button type="button" class="js-row-remove-button"></button> + </li> + </ul> + <button type="button" class="js-secret-value-reveal-button"> + Reveal values + </button> + </form>`); + + variableList = new VariableList({ + container: $wrapper, + formField: 'variables', + }); + variableList.init(); + }); + + it('should add another row when editing the last rows protected checkbox', (done) => { + const $row = $wrapper.find('.js-row:last-child'); + $row.find('.ci-variable-protected-item .js-project-feature-toggle').click(); + + getSetTimeoutPromise() + .then(() => { + expect($wrapper.find('.js-row').length).toBe(2); + + // Check for the correct default in the new row + const $protectedInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-protected'); + expect($protectedInput.val()).toBe('true'); + }) + .then(done) + .catch(done.fail); + }); + }); +}); diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js new file mode 100644 index 00000000000..eb508a7f059 --- /dev/null +++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js @@ -0,0 +1,30 @@ +import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list'; + +describe('NativeFormVariableList', () => { + preloadFixtures('pipeline_schedules/edit.html.raw'); + + let $wrapper; + + beforeEach(() => { + loadFixtures('pipeline_schedules/edit.html.raw'); + $wrapper = $('.js-ci-variable-list-section'); + + setupNativeFormVariableList({ + container: $wrapper, + formField: 'schedule', + }); + }); + + describe('onFormSubmit', () => { + it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { + const $row = $wrapper.find('.js-row'); + expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('schedule[variables_attributes][][key]'); + expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][value]'); + + $wrapper.closest('form').trigger('trigger-submit'); + + expect($row.find('.js-ci-variable-input-key').attr('name')).toBe(''); + expect($row.find('.js-ci-variable-input-value').attr('name')).toBe(''); + }); + }); +}); diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb new file mode 100644 index 00000000000..56f27ea7df1 --- /dev/null +++ b/spec/javascripts/fixtures/pipeline_schedules.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project, :public, :repository) } + let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: admin) } + let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: admin) } + let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) } + let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) } + + render_views + + before(:all) do + clean_frontend_fixtures('pipeline_schedules/') + end + + before do + sign_in(admin) + end + + it 'pipeline_schedules/edit.html.raw' do |example| + get :edit, + namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule.id + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end + + it 'pipeline_schedules/edit_with_variables.html.raw' do |example| + get :edit, + namespace_id: project.namespace.to_param, + project_id: project, + id: pipeline_schedule_populated.id + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index 6f357306ec7..50a587ef351 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -130,16 +130,25 @@ describe('GfmAutoComplete', function () { }); describe('should not match special sequences', () => { - const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + const shouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']); + const shouldNotBePrependedBy = ['`']; flagsUseDefaultMatcher.forEach((atSign) => { - ShouldNotBeFollowedBy.forEach((followedSymbol) => { + shouldNotBeFollowedBy.forEach((followedSymbol) => { const seq = atSign + followedSymbol; it(`should not match "${seq}"`, () => { expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); }); }); + + shouldNotBePrependedBy.forEach((prependedSymbol) => { + const seq = prependedSymbol + atSign; + + it(`should not match "${seq}"`, () => { + expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); + }); + }); }); }); }); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 2cd2e63b15d..5245bf6455c 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,6 @@ /* 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(); + }); }); }); }); diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js index 0452934ea9e..03b58e9c1d0 100644 --- a/spec/javascripts/job_spec.js +++ b/spec/javascripts/job_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import '~/lib/utils/datetime_utility'; @@ -6,11 +8,29 @@ import '~/breakpoints'; describe('Job', () => { const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`; + let mock; + let response; + + function waitForPromise() { + return new Promise(resolve => requestAnimationFrame(resolve)); + } preloadFixtures('builds/build-with-artifacts.html.raw'); beforeEach(() => { loadFixtures('builds/build-with-artifacts.html.raw'); + + spyOn(urlUtils, 'visitUrl'); + + mock = new MockAdapter(axios); + + mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]); + }); + + afterEach(() => { + mock.restore(); + + response = {}; }); describe('class constructor', () => { @@ -55,161 +75,159 @@ describe('Job', () => { }); describe('running build', () => { - it('updates the build trace on an interval', function () { - const deferred1 = $.Deferred(); - const deferred2 = $.Deferred(); - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise()); - spyOn(urlUtils, 'visitUrl'); - - deferred1.resolve({ + it('updates the build trace on an interval', function (done) { + response = { html: '<span>Update<span>', status: 'running', state: 'newstate', append: true, complete: false, - }); - - deferred2.resolve({ - html: '<span>More</span>', - status: 'running', - state: 'finalstate', - append: true, - complete: true, - }); + }; this.job = new Job(); - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - expect(this.job.state).toBe('newstate'); - - jasmine.clock().tick(4001); - - expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); - expect(this.job.state).toBe('finalstate'); + waitForPromise() + .then(() => { + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + expect(this.job.state).toBe('newstate'); + + response = { + html: '<span>More</span>', + status: 'running', + state: 'finalstate', + append: true, + complete: true, + }; + }) + .then(() => jasmine.clock().tick(4001)) + .then(waitForPromise) + .then(() => { + expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/); + expect(this.job.state).toBe('finalstate'); + }) + .then(done) + .catch(done.fail); }); - it('replaces the entire build trace', () => { - const deferred1 = $.Deferred(); - const deferred2 = $.Deferred(); - - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise()); - - spyOn(urlUtils, 'visitUrl'); - - deferred1.resolve({ + it('replaces the entire build trace', (done) => { + response = { html: '<span>Update<span>', status: 'running', append: false, complete: false, - }); - - deferred2.resolve({ - html: '<span>Different</span>', - status: 'running', - append: false, - }); + }; this.job = new Job(); - expect($('#build-trace .js-build-output').text()).toMatch(/Update/); - - jasmine.clock().tick(4001); - - expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); - expect($('#build-trace .js-build-output').text()).toMatch(/Different/); + waitForPromise() + .then(() => { + expect($('#build-trace .js-build-output').text()).toMatch(/Update/); + + response = { + html: '<span>Different</span>', + status: 'running', + append: false, + }; + }) + .then(() => jasmine.clock().tick(4001)) + .then(waitForPromise) + .then(() => { + expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/); + expect($('#build-trace .js-build-output').text()).toMatch(/Different/); + }) + .then(done) + .catch(done.fail); }); }); describe('truncated information', () => { describe('when size is less than total', () => { - it('shows information about truncated log', () => { - spyOn(urlUtils, 'visitUrl'); - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - - deferred.resolve({ + it('shows information about truncated log', (done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); - expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + waitForPromise() + .then(() => { + expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden'); + }) + .then(done) + .catch(done.fail); }); - it('shows the size in KiB', () => { + it('shows the size in KiB', (done) => { const size = 50; - spyOn(urlUtils, 'visitUrl'); - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + response = { html: '<span>Update</span>', status: 'success', append: false, size, total: 100, - }); + }; this.job = new Job(); - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${numberToHumanSize(size)}`); + waitForPromise() + .then(() => { + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${numberToHumanSize(size)}`); + }) + .then(done) + .catch(done.fail); }); - it('shows incremented size', () => { - const deferred1 = $.Deferred(); - const deferred2 = $.Deferred(); - - spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise()); - - spyOn(urlUtils, 'visitUrl'); - - deferred1.resolve({ + it('shows incremented size', (done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${numberToHumanSize(50)}`); - - jasmine.clock().tick(4001); - - deferred2.resolve({ - html: '<span>Update</span>', - status: 'success', - append: true, - size: 10, - total: 100, - }); - - expect( - document.querySelector('.js-truncated-info-size').textContent.trim(), - ).toEqual(`${numberToHumanSize(60)}`); + waitForPromise() + .then(() => { + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${numberToHumanSize(50)}`); + + response = { + html: '<span>Update</span>', + status: 'success', + append: true, + size: 10, + total: 100, + }; + }) + .then(() => jasmine.clock().tick(4001)) + .then(waitForPromise) + .then(() => { + expect( + document.querySelector('.js-truncated-info-size').textContent.trim(), + ).toEqual(`${numberToHumanSize(60)}`); + }) + .then(done) + .catch(done.fail); }); it('renders the raw link', () => { - const deferred = $.Deferred(); - spyOn(urlUtils, 'visitUrl'); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); @@ -220,50 +238,50 @@ describe('Job', () => { }); describe('when size is equal than total', () => { - it('does not show the trunctated information', () => { - const deferred = $.Deferred(); - spyOn(urlUtils, 'visitUrl'); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + it('does not show the trunctated information', (done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 100, total: 100, - }); + }; this.job = new Job(); - expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + waitForPromise() + .then(() => { + expect(document.querySelector('.js-truncated-info').classList).toContain('hidden'); + }) + .then(done) + .catch(done.fail); }); }); }); describe('output trace', () => { - beforeEach(() => { - const deferred = $.Deferred(); - spyOn(urlUtils, 'visitUrl'); - - spyOn($, 'ajax').and.returnValue(deferred.promise()); - deferred.resolve({ + beforeEach((done) => { + response = { html: '<span>Update</span>', status: 'success', append: false, size: 50, total: 100, - }); + }; this.job = new Job(); + + waitForPromise() + .then(done) + .catch(done.fail); }); it('should render trace controls', () => { const controllers = document.querySelector('.controllers'); - expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined(); - expect(controllers.querySelector('.js-erase-link')).toBeDefined(); - expect(controllers.querySelector('.js-scroll-up')).toBeDefined(); - expect(controllers.querySelector('.js-scroll-down')).toBeDefined(); + expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull(); + expect(controllers.querySelector('.js-scroll-up')).not.toBeNull(); + expect(controllers.querySelector('.js-scroll-down')).not.toBeNull(); }); it('should render received output', () => { @@ -276,13 +294,13 @@ describe('Job', () => { describe('getBuildTrace', () => { it('should request build trace with state parameter', (done) => { - spyOn(jQuery, 'ajax').and.callThrough(); + spyOn(axios, 'get').and.callThrough(); // eslint-disable-next-line no-new new Job(); setTimeout(() => { - expect(jQuery.ajax).toHaveBeenCalledWith( - { url: `${JOB_URL}/trace.json`, data: { state: '' } }, + expect(axios.get).toHaveBeenCalledWith( + `${JOB_URL}/trace.json`, { params: { state: '' } }, ); done(); }, 0); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js index a197b35f6fb..7d992f62f64 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js +++ b/spec/javascripts/labels_issue_sidebar_spec.js @@ -1,4 +1,6 @@ /* 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(); diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js index 49971bd91e2..7603400b55e 100644 --- a/spec/javascripts/lib/utils/ajax_cache_spec.js +++ b/spec/javascripts/lib/utils/ajax_cache_spec.js @@ -1,3 +1,5 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import AjaxCache from '~/lib/utils/ajax_cache'; describe('AjaxCache', () => { @@ -87,66 +89,53 @@ describe('AjaxCache', () => { }); describe('retrieve', () => { - let ajaxSpy; + let mock; beforeEach(() => { - spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url)); + mock = new MockAdapter(axios); + + spyOn(axios, 'get').and.callThrough(); + }); + + afterEach(() => { + mock.restore(); }); it('stores and returns data from Ajax call if cache is empty', (done) => { - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - const deferred = $.Deferred(); - deferred.resolve(dummyResponse); - return deferred.promise(); - }; + mock.onGet(dummyEndpoint).reply(200, dummyResponse); AjaxCache.retrieve(dummyEndpoint) .then((data) => { - expect(data).toBe(dummyResponse); - expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse); + expect(data).toEqual(dummyResponse); + expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse); }) .then(done) .catch(fail); }); - it('makes no Ajax call if request is pending', () => { - const responseDeferred = $.Deferred(); - - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - // neither reject nor resolve to keep request pending - return responseDeferred.promise(); - }; - - const unexpectedResponse = data => fail(`Did not expect response: ${data}`); + it('makes no Ajax call if request is pending', (done) => { + mock.onGet(dummyEndpoint).reply(200, dummyResponse); AjaxCache.retrieve(dummyEndpoint) - .then(unexpectedResponse) + .then(done) .catch(fail); AjaxCache.retrieve(dummyEndpoint) - .then(unexpectedResponse) + .then(done) .catch(fail); - expect($.ajax.calls.count()).toBe(1); + expect(axios.get.calls.count()).toBe(1); }); it('returns undefined if Ajax call fails and cache is empty', (done) => { - const dummyStatusText = 'exploded'; - const dummyErrorMessage = 'server exploded'; - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - const deferred = $.Deferred(); - deferred.reject(null, dummyStatusText, dummyErrorMessage); - return deferred.promise(); - }; + const errorMessage = 'Network Error'; + mock.onGet(dummyEndpoint).networkError(); AjaxCache.retrieve(dummyEndpoint) .then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`)) .catch((error) => { - expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`); - expect(error.textStatus).toBe(dummyStatusText); + expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`); + expect(error.textStatus).toBe(errorMessage); done(); }) .catch(fail); @@ -154,7 +143,9 @@ describe('AjaxCache', () => { it('makes no Ajax call if matching data exists', (done) => { AjaxCache.internalStorage[dummyEndpoint] = dummyResponse; - ajaxSpy = () => fail(new Error('expected no Ajax call!')); + mock.onGet(dummyEndpoint).reply(() => { + fail(new Error('expected no Ajax call!')); + }); AjaxCache.retrieve(dummyEndpoint) .then((data) => { @@ -171,12 +162,7 @@ describe('AjaxCache', () => { AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse; - ajaxSpy = (url) => { - expect(url).toBe(dummyEndpoint); - const deferred = $.Deferred(); - deferred.resolve(dummyResponse); - return deferred.promise(); - }; + mock.onGet(dummyEndpoint).reply(200, dummyResponse); // Call without forceRetrieve param AjaxCache.retrieve(dummyEndpoint) @@ -189,7 +175,7 @@ describe('AjaxCache', () => { // Call with forceRetrieve param AjaxCache.retrieve(dummyEndpoint, true) .then((data) => { - expect(data).toBe(dummyResponse); + expect(data).toEqual(dummyResponse); }) .then(done) .catch(fail); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js index 1052b4e7c20..80430011aed 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js +++ b/spec/javascripts/lib/utils/common_utils_spec.js @@ -1,7 +1,6 @@ /* eslint-disable promise/catch-or-return */ - -import * as commonUtils from '~/lib/utils/common_utils'; import axios from '~/lib/utils/axios_utils'; +import * as commonUtils from '~/lib/utils/common_utils'; import MockAdapter from 'axios-mock-adapter'; describe('common_utils', () => { @@ -460,17 +459,6 @@ describe('common_utils', () => { }); }); - describe('ajaxPost', () => { - it('should perform `$.ajax` call and do `POST` request', () => { - const requestURL = '/some/random/api'; - const data = { keyname: 'value' }; - const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {}); - - commonUtils.ajaxPost(requestURL, data); - expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST'); - }); - }); - describe('spriteIcon', () => { let beforeGon; diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index a6be474805b..fda24db98b4 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,5 +1,6 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ - +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import MergeRequestTabs from '~/merge_request_tabs'; import '~/commit/pipelines/pipelines_bundle'; @@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo'; describe('activateTab', function () { beforeEach(function () { - spyOn($, 'ajax').and.callFake(function () {}); + spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); this.subject = this.class.activateTab; }); @@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo'; describe('setCurrentAction', function () { beforeEach(function () { - spyOn($, 'ajax').and.callFake(function () {}); + spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} })); this.subject = this.class.setCurrentAction; }); @@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo'; }); describe('tabShown', () => { + let mock; + beforeEach(function () { - spyOn($, 'ajax').and.callFake(function (options) { - options.success({ html: '' }); + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/diffs\.json/).reply(200, { + data: { html: '' }, }); + loadFixtures('merge_requests/merge_request_with_task_list.html.raw'); }); + afterEach(() => { + mock.restore(); + }); + describe('with "Side-by-side"/parallel diff view', () => { beforeEach(function () { this.class.diffViewType = () => 'parallel'; @@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo'; it('triggers Ajax request to JSON endpoint', function (done) { const url = '/foo/bar/merge_requests/1/diffs'; - spyOn(this.class, 'ajaxGet').and.callFake((options) => { - expect(options.url).toEqual(`${url}.json`); + + spyOn(axios, 'get').and.callFake((reqUrl) => { + expect(reqUrl).toBe(`${url}.json`); + done(); + + return Promise.resolve({ data: {} }); }); this.class.loadDiff(url); }); it('triggers scroll event when diff already loaded', function (done) { - spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail()); + spyOn(axios, 'get').and.callFake(done.fail); spyOn(document, 'dispatchEvent'); this.class.diffsLoaded = true; @@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo'; describe('with inline diff', () => { let noteId; let noteLineNumId; + let mock; beforeEach(() => { const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture); @@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo'; .attr('href') .replace('#', ''); - spyOn($, 'ajax').and.callFake(function (options) { - options.success(diffsResponse); - }); + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); + }); + + afterEach(() => { + mock.restore(); }); describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function () { + it('should expand and scroll to linked fragment hash #note_xxx', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'old', - forceShow: true, + setTimeout(() => { + expect(noteId.length).toBeGreaterThan(0); + expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'old', + forceShow: true, + }); + + done(); }); }); - it('should gracefully ignore non-existant fragment hash', function () { + it('should gracefully ignore non-existant fragment hash', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + setTimeout(() => { + expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + + done(); + }); }); }); @@ -370,6 +395,7 @@ import 'vendor/jquery.scrollTo'; describe('with parallel diff', () => { let noteId; let noteLineNumId; + let mock; beforeEach(() => { const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture); @@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo'; .attr('href') .replace('#', ''); - spyOn($, 'ajax').and.callFake(function (options) { - options.success(diffsResponse); - }); + mock = new MockAdapter(axios); + mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse); + }); + + afterEach(() => { + mock.restore(); }); describe('with note fragment hash', () => { - it('should expand and scroll to linked fragment hash #note_xxx', function () { + it('should expand and scroll to linked fragment hash #note_xxx', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(noteId.length).toBeGreaterThan(0); - expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ - target: jasmine.any(Object), - lineType: 'new', - forceShow: true, + setTimeout(() => { + expect(noteId.length).toBeGreaterThan(0); + expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({ + target: jasmine.any(Object), + lineType: 'new', + forceShow: true, + }); + + done(); }); }); - it('should gracefully ignore non-existant fragment hash', function () { + it('should gracefully ignore non-existant fragment hash', function (done) { spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist'); this.class.loadDiff('/foo/bar/merge_requests/1/diffs'); - expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + setTimeout(() => { + expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled(); + done(); + }); }); }); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index a40821a5693..2fb385bd79f 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,11 +1,14 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */ import _ from 'underscore'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; import * as urlUtils from '~/lib/utils/url_utility'; import 'autosize'; import '~/gl_form'; import '~/lib/utils/text_utility'; import '~/render_gfm'; import Notes from '~/notes'; +import timeoutPromise from './helpers/set_timeout_promise_helper'; (function() { window.gon || (window.gon = {}); @@ -119,6 +122,7 @@ import Notes from '~/notes'; let noteEntity; let $form; let $notesContainer; + let mock; beforeEach(() => { this.notes = new Notes('', []); @@ -136,24 +140,32 @@ import Notes from '~/notes'; $form = $('form.js-main-target-form'); $notesContainer = $('ul.main-notes-list'); $form.find('textarea.js-note-text').val(sampleComment); + + mock = new MockAdapter(axios); + mock.onPost(/(.*)\/notes$/).reply(200, noteEntity); }); - it('updates note and resets edit form', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); + + it('updates note and resets edit form', (done) => { spyOn(this.notes, 'revertNoteEditForm'); spyOn(this.notes, 'setupNewNote'); $('.js-comment-button').click(); - deferred.resolve(noteEntity); - const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); - const updatedNote = Object.assign({}, noteEntity); - updatedNote.note = 'bar'; - this.notes.updateNote(updatedNote, $targetNote); + setTimeout(() => { + const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`); + const updatedNote = Object.assign({}, noteEntity); + updatedNote.note = 'bar'; + this.notes.updateNote(updatedNote, $targetNote); + + expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); + expect(this.notes.setupNewNote).toHaveBeenCalled(); - expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote); - expect(this.notes.setupNewNote).toHaveBeenCalled(); + done(); + }); }); }); @@ -479,8 +491,19 @@ import Notes from '~/notes'; }; let $form; let $notesContainer; + let mock; + + function mockNotesPost() { + mock.onPost(/(.*)\/notes$/).reply(200, note); + } + + function mockNotesPostError() { + mock.onPost(/(.*)\/notes$/).networkError(); + } beforeEach(() => { + mock = new MockAdapter(axios); + this.notes = new Notes('', []); window.gon.current_username = 'root'; window.gon.current_user_fullname = 'Administrator'; @@ -489,63 +512,92 @@ import Notes from '~/notes'; $form.find('textarea.js-note-text').val(sampleComment); }); + afterEach(() => { + mock.restore(); + }); + it('should show placeholder note while new comment is being posted', () => { + mockNotesPost(); + $('.js-comment-button').click(); expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true); }); - it('should remove placeholder note when new comment is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should remove placeholder note when new comment is done posting', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - expect($notesContainer.find('.note.being-posted').length).toEqual(0); + setTimeout(() => { + expect($notesContainer.find('.note.being-posted').length).toEqual(0); + + done(); + }); }); - it('should show actual note element when new comment is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should show actual note element when new comment is done posting', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); + setTimeout(() => { + expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true); + + done(); + }); }); - it('should reset Form when new comment is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should reset Form when new comment is done posting', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - expect($form.find('textarea.js-note-text').val()).toEqual(''); + setTimeout(() => { + expect($form.find('textarea.js-note-text').val()).toEqual(''); + + done(); + }); }); - it('should show flash error message when new comment failed to be posted', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should show flash error message when new comment failed to be posted', (done) => { + mockNotesPostError(); + $('.js-comment-button').click(); - deferred.reject(); - expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true); + setTimeout(() => { + expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true); + + done(); + }); }); - it('should show flash error message when comment failed to be updated', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + it('should show flash error message when comment failed to be updated', (done) => { + mockNotesPost(); + $('.js-comment-button').click(); - deferred.resolve(note); - const $noteEl = $notesContainer.find(`#note_${note.id}`); - $noteEl.find('.js-note-edit').click(); - $noteEl.find('textarea.js-note-text').val(updatedComment); - $noteEl.find('.js-comment-save-button').click(); + timeoutPromise() + .then(() => { + const $noteEl = $notesContainer.find(`#note_${note.id}`); + $noteEl.find('.js-note-edit').click(); + $noteEl.find('textarea.js-note-text').val(updatedComment); - deferred.reject(); - const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`); - expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals - expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original - expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown + mock.restore(); + + mockNotesPostError(); + + $noteEl.find('.js-comment-save-button').click(); + }) + .then(timeoutPromise) + .then(() => { + const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`); + expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals + expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original + expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown + + done(); + }) + .catch(done.fail); }); }); @@ -563,8 +615,12 @@ import Notes from '~/notes'; }; let $form; let $notesContainer; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); + mock.onPost(/(.*)\/notes$/).reply(200, note); + this.notes = new Notes('', []); window.gon.current_username = 'root'; window.gon.current_user_fullname = 'Administrator'; @@ -582,15 +638,20 @@ import Notes from '~/notes'; $form.find('textarea.js-note-text').val(sampleComment); }); - it('should remove slash command placeholder when comment with slash commands is done posting', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); + + it('should remove slash command placeholder when comment with slash commands is done posting', (done) => { spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough(); $('.js-comment-button').click(); expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown - deferred.resolve(note); - expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + + setTimeout(() => { + expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed + done(); + }); }); }); @@ -607,8 +668,12 @@ import Notes from '~/notes'; }; let $form; let $notesContainer; + let mock; beforeEach(() => { + mock = new MockAdapter(axios); + mock.onPost(/(.*)\/notes$/).reply(200, note); + this.notes = new Notes('', []); window.gon.current_username = 'root'; window.gon.current_user_fullname = 'Administrator'; @@ -617,19 +682,24 @@ import Notes from '~/notes'; $form.find('textarea.js-note-text').html(sampleComment); }); - it('should not render a script tag', () => { - const deferred = $.Deferred(); - spyOn($, 'ajax').and.returnValue(deferred.promise()); + afterEach(() => { + mock.restore(); + }); + + it('should not render a script tag', (done) => { $('.js-comment-button').click(); - deferred.resolve(note); - const $noteEl = $notesContainer.find(`#note_${note.id}`); - $noteEl.find('.js-note-edit').click(); - $noteEl.find('textarea.js-note-text').html(updatedComment); - $noteEl.find('.js-comment-save-button').click(); + setTimeout(() => { + const $noteEl = $notesContainer.find(`#note_${note.id}`); + $noteEl.find('.js-note-edit').click(); + $noteEl.find('textarea.js-note-text').html(updatedComment); + $noteEl.find('.js-comment-save-button').click(); + + const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); + expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); - const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container'); - expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(''); + done(); + }); }); }); diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js deleted file mode 100644 index 5b316b319a5..00000000000 --- a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import { - setupPipelineVariableList, - insertRow, - removeRow, -} from '~/pipeline_schedules/setup_pipeline_variable_list'; - -describe('Pipeline Variable List', () => { - let $markup; - - describe('insertRow', () => { - it('should insert another row', () => { - $markup = $(`<div> - <li class="js-row"> - <input> - <textarea></textarea> - </li> - </div>`); - - insertRow($markup.find('.js-row')); - - expect($markup.find('.js-row').length).toBe(2); - }); - - it('should clear `data-is-persisted` on cloned row', () => { - $markup = $(`<div> - <li class="js-row" data-is-persisted="true"></li> - </div>`); - - insertRow($markup.find('.js-row')); - - const $lastRow = $markup.find('.js-row').last(); - expect($lastRow.attr('data-is-persisted')).toBe(undefined); - }); - - it('should clear inputs on cloned row', () => { - $markup = $(`<div> - <li class="js-row"> - <input value="foo"> - <textarea>bar</textarea> - </li> - </div>`); - - insertRow($markup.find('.js-row')); - - const $lastRow = $markup.find('.js-row').last(); - expect($lastRow.find('input').val()).toBe(''); - expect($lastRow.find('textarea').val()).toBe(''); - }); - }); - - describe('removeRow', () => { - it('should remove dynamic row', () => { - $markup = $(`<div> - <li class="js-row"> - <input> - <textarea></textarea> - </li> - </div>`); - - removeRow($markup.find('.js-row')); - - expect($markup.find('.js-row').length).toBe(0); - }); - - it('should hide and mark to destroy with already persisted rows', () => { - $markup = $(`<div> - <li class="js-row" data-is-persisted="true"> - <input class="js-destroy-input"> - </li> - </div>`); - - const $row = $markup.find('.js-row'); - removeRow($row); - - expect($row.find('.js-destroy-input').val()).toBe('1'); - expect($markup.find('.js-row').length).toBe(1); - }); - }); - - describe('setupPipelineVariableList', () => { - beforeEach(() => { - $markup = $(`<form> - <li class="js-row"> - <input class="js-user-input" name="schedule[variables_attributes][][key]"> - <textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea> - <button class="js-row-remove-button"></button> - <button class="js-row-add-button"></button> - </li> - </form>`); - - setupPipelineVariableList($markup); - }); - - it('should remove the row when clicking the remove button', () => { - $markup.find('.js-row-remove-button').trigger('click'); - - expect($markup.find('.js-row').length).toBe(0); - }); - - it('should add another row when editing the last rows key input', () => { - const $row = $markup.find('.js-row'); - $row.find('input.js-user-input') - .val('foo') - .trigger('input'); - - expect($markup.find('.js-row').length).toBe(2); - }); - - it('should add another row when editing the last rows value textarea', () => { - const $row = $markup.find('.js-row'); - $row.find('textarea.js-user-input') - .val('foo') - .trigger('input'); - - expect($markup.find('.js-row').length).toBe(2); - }); - - it('should remove empty row after blurring', () => { - const $row = $markup.find('.js-row'); - $row.find('input.js-user-input') - .val('foo') - .trigger('input'); - - expect($markup.find('.js-row').length).toBe(2); - - $row.find('input.js-user-input') - .val('') - .trigger('input') - .trigger('blur'); - - expect($markup.find('.js-row').length).toBe(1); - }); - - it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => { - const $row = $markup.find('.js-row'); - expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]'); - expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]'); - - $markup.filter('form').submit(); - - expect($row.find('input').attr('name')).toBe(''); - expect($row.find('textarea').attr('name')).toBe(''); - }); - }); -}); diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb new file mode 100644 index 00000000000..a1cb0c07b06 --- /dev/null +++ b/spec/lib/banzai/color_parser_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Banzai::ColorParser do + describe '.parse' do + context 'HEX format' do + [ + '#abc', '#ABC', + '#d2d2d2', '#D2D2D2', + '#123a', '#123A', + '#123456aa', '#123456AA' + ].each do |color| + it "parses the valid hex color #{color}" do + expect(subject.parse(color)).to eq(color) + end + end + + [ + '#', '#1', '#12', '#12g', '#12G', + '#12345', '#r2r2r2', '#R2R2R2', '#1234567', + '# 123', '# 1234', '# 123456', '# 12345678', + '#1 2 3', '#123 4', '#12 34 56', '#123456 78' + ].each do |color| + it "does not parse the invalid hex color #{color}" do + expect(subject.parse(color)).to be_nil + end + end + end + + context 'RGB format' do + [ + 'rgb(0,0,0)', 'rgb(255,255,255)', + 'rgb(0, 0, 0)', 'RGB(0,0,0)', + 'rgb(0,0,0,0)', 'rgb(0,0,0,0.0)', 'rgb(0,0,0,.0)', + 'rgb(0,0,0, 0)', 'rgb(0,0,0, 0.0)', 'rgb(0,0,0, .0)', + 'rgb(0,0,0,1)', 'rgb(0,0,0,1.0)', + 'rgba(0,0,0)', 'rgba(0,0,0,0)', 'RGBA(0,0,0)', + 'rgb(0%,0%,0%)', 'rgba(0%,0%,0%,0%)' + ].each do |color| + it "parses the valid rgb color #{color}" do + expect(subject.parse(color)).to eq(color) + end + end + + [ + 'FOOrgb(0,0,0)', 'rgb(0,0,0)BAR', + 'rgb(0,0,-1)', 'rgb(0,0,-0)', 'rgb(0,0,256)', + 'rgb(0,0,0,-0.1)', 'rgb(0,0,0,-0.0)', 'rgb(0,0,0,-.1)', + 'rgb(0,0,0,1.1)', 'rgb(0,0,0,2)', + 'rgba(0,0,0,)', 'rgba(0,0,0,0.)', 'rgba(0,0,0,1.)', + 'rgb(0,0,0%)', 'rgb(101%,0%,0%)' + ].each do |color| + it "does not parse the invalid rgb color #{color}" do + expect(subject.parse(color)).to be_nil + end + end + end + + context 'HSL format' do + [ + 'hsl(0,0%,0%)', 'hsl(0,100%,100%)', + 'hsl(540,0%,0%)', 'hsl(-720,0%,0%)', + 'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)', + 'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)', + 'hsl(0,0%,0%,0)', 'hsl(0,0%,0%,0.0)', 'hsl(0,0%,0%,.0)', + 'hsl(0,0%,0%, 0)', 'hsl(0,0%,0%, 0.0)', 'hsl(0,0%,0%, .0)', + 'hsl(0,0%,0%,1)', 'hsl(0,0%,0%,1.0)', + 'hsla(0,0%,0%)', 'hsla(0,0%,0%,0)', 'HSLA(0,0%,0%)', + 'hsl(1rad,0%,0%)', 'hsl(1.1rad,0%,0%)', 'hsl(.1rad,0%,0%)', + 'hsl(-1rad,0%,0%)', 'hsl(1RAD,0%,0%)' + ].each do |color| + it "parses the valid hsl color #{color}" do + expect(subject.parse(color)).to eq(color) + end + end + + [ + 'hsl(+0,0%,0%)', 'hsl(0,0,0%)', 'hsl(0,0%,0)', 'hsl(0 deg,0%,0%)', + 'hsl(0,-0%,0%)', 'hsl(0,101%,0%)', 'hsl(0,-1%,0%)', + 'hsl(0,0%,0%,-0.1)', 'hsl(0,0%,0%,-.1)', + 'hsl(0,0%,0%,1.1)', 'hsl(0,0%,0%,2)', + 'hsl(0,0%,0%,)', 'hsl(0,0%,0%,0.)', 'hsl(0,0%,0%,1.)', + 'hsl(deg,0%,0%)', 'hsl(rad,0%,0%)' + ].each do |color| + it "does not parse the invalid hsl color #{color}" do + expect(subject.parse(color)).to be_nil + end + end + end + end +end diff --git a/spec/lib/banzai/filter/color_filter_spec.rb b/spec/lib/banzai/filter/color_filter_spec.rb new file mode 100644 index 00000000000..a098b037510 --- /dev/null +++ b/spec/lib/banzai/filter/color_filter_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Banzai::Filter::ColorFilter, lib: true do + include FilterSpecHelper + + let(:color) { '#F00' } + let(:color_chip_selector) { 'code > span.gfm-color_chip > span' } + + ['#123', '#1234', '#123456', '#12345678', + 'rgb(0,0,0)', 'RGB(0, 0, 0)', 'rgba(0,0,0,1)', 'RGBA(0,0,0,0.7)', + 'hsl(270,30%,50%)', 'HSLA(270, 30%, 50%, .7)'].each do |color| + it "inserts color chip for supported color format #{color}" do + content = code_tag(color) + doc = filter(content) + color_chip = doc.at_css(color_chip_selector) + + expect(color_chip.content).to be_empty + expect(color_chip.parent[:class]).to eq 'gfm-color_chip' + expect(color_chip[:style]).to eq "background-color: #{color};" + end + end + + it 'ignores valid color code without backticks(code tags)' do + doc = filter(color) + + expect(doc.css('span.gfm-color_chip').size).to be_zero + end + + it 'ignores valid color code with prepended space' do + content = code_tag(' ' + color) + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + it 'ignores valid color code with appended space' do + content = code_tag(color + ' ') + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + it 'ignores valid color code surrounded by spaces' do + content = code_tag(' ' + color + ' ') + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + it 'ignores invalid color code' do + invalid_color = '#BAR' + content = code_tag(invalid_color) + doc = filter(content) + + expect(doc.css(color_chip_selector).size).to be_zero + end + + def code_tag(string) + "<code>#{string}</code>" + end +end diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb index 383bae6e087..d9c21a22590 100644 --- a/spec/lib/gitlab/badge/coverage/template_spec.rb +++ b/spec/lib/gitlab/badge/coverage/template_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::Badge::Coverage::Template do - let(:badge) { double(entity: 'coverage', status: 90) } + let(:badge) { double(entity: 'coverage', status: 90.00) } let(:template) { described_class.new(badge) } describe '#key_text' do @@ -13,7 +13,17 @@ describe Gitlab::Badge::Coverage::Template do describe '#value_text' do context 'when coverage is known' do it 'returns coverage percentage' do - expect(template.value_text).to eq '90%' + expect(template.value_text).to eq '90.00%' + end + end + + context 'when coverage is known to many digits' do + before do + allow(badge).to receive(:status).and_return(92.349) + end + + it 'returns rounded coverage percentage' do + expect(template.value_text).to eq '92.35%' end end @@ -37,7 +47,7 @@ describe Gitlab::Badge::Coverage::Template do describe '#value_width' do context 'when coverage is known' do it 'is narrower when coverage is known' do - expect(template.value_width).to eq 36 + expect(template.value_width).to eq 54 end end @@ -113,7 +123,7 @@ describe Gitlab::Badge::Coverage::Template do describe '#width' do context 'when coverage is known' do it 'returns the key width plus value width' do - expect(template.width).to eq 98 + expect(template.width).to eq 116 end end diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb index 492659a82b0..4ddcbd7eb66 100644 --- a/spec/lib/gitlab/current_settings_spec.rb +++ b/spec/lib/gitlab/current_settings_spec.rb @@ -8,22 +8,37 @@ describe Gitlab::CurrentSettings do end describe '#current_application_settings' do + it 'allows keys to be called directly' do + db_settings = create(:application_setting, + home_page_url: 'http://mydomain.com', + signup_enabled: false) + + expect(described_class.home_page_url).to eq(db_settings.home_page_url) + expect(described_class.signup_enabled?).to be_falsey + expect(described_class.signup_enabled).to be_falsey + expect(described_class.metrics_sample_interval).to be(15) + end + context 'with DB available' do before do - allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true) + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true) + allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original + allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true) end it 'attempts to use cached values first' do expect(ApplicationSetting).to receive(:cached) - expect(current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).to be_a(ApplicationSetting) end it 'falls back to DB if Redis returns an empty value' do expect(ApplicationSetting).to receive(:cached).and_return(nil) expect(ApplicationSetting).to receive(:last).and_call_original.twice - expect(current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).to be_a(ApplicationSetting) end it 'falls back to DB if Redis fails' do @@ -32,14 +47,14 @@ describe Gitlab::CurrentSettings do expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError) - expect(current_application_settings).to eq(db_settings) + expect(described_class.current_application_settings).to eq(db_settings) end it 'creates default ApplicationSettings if none are present' do expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError) expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError) - settings = current_application_settings + settings = described_class.current_application_settings expect(settings).to be_a(ApplicationSetting) expect(settings).to be_persisted @@ -52,7 +67,7 @@ describe Gitlab::CurrentSettings do end it 'returns an in-memory ApplicationSetting object' do - settings = current_application_settings + settings = described_class.current_application_settings expect(settings).to be_a(OpenStruct) expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled) @@ -63,7 +78,7 @@ describe Gitlab::CurrentSettings do db_settings = create(:application_setting, home_page_url: 'http://mydomain.com', signup_enabled: false) - settings = current_application_settings + settings = described_class.current_application_settings app_defaults = ApplicationSetting.last expect(settings).to be_a(OpenStruct) @@ -80,15 +95,16 @@ describe Gitlab::CurrentSettings do context 'with DB unavailable' do before do - allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false) - allow_any_instance_of(described_class).to receive(:retrieve_settings_from_database_cache?).and_return(nil) + # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues + # during the initialization phase of the test suite, so instead let's mock the internals of it + allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false) end it 'returns an in-memory ApplicationSetting object' do expect(ApplicationSetting).not_to receive(:current) expect(ApplicationSetting).not_to receive(:last) - expect(current_application_settings).to be_a(OpenStruct) + expect(described_class.current_application_settings).to be_a(OpenStruct) end end @@ -101,8 +117,8 @@ describe Gitlab::CurrentSettings do expect(ApplicationSetting).not_to receive(:current) expect(ApplicationSetting).not_to receive(:last) - expect(current_application_settings).to be_a(ApplicationSetting) - expect(current_application_settings).not_to be_persisted + expect(described_class.current_application_settings).to be_a(ApplicationSetting) + expect(described_class.current_application_settings).not_to be_persisted end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 96a442f782f..ec1c7a96f92 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1162,14 +1162,27 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding a branch' - it 'should reload Rugged::Repository and return master' do - expect(Rugged::Repository).to receive(:new).twice.and_call_original + context 'force_reload is true' do + it 'should reload Rugged::Repository' do + expect(Rugged::Repository).to receive(:new).twice.and_call_original - repository.find_branch('master') - branch = repository.find_branch('master', force_reload: true) + repository.find_branch('master') + branch = repository.find_branch('master', force_reload: true) - expect(branch).to be_a_kind_of(Gitlab::Git::Branch) - expect(branch.name).to eq('master') + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end + end + + context 'force_reload is false' do + it 'should not reload Rugged::Repository' do + expect(Rugged::Repository).to receive(:new).once.and_call_original + + branch = repository.find_branch('master', force_reload: false) + + expect(branch).to be_a_kind_of(Gitlab::Git::Branch) + expect(branch.name).to eq('master') + end end end end @@ -2183,7 +2196,7 @@ describe Gitlab::Git::Repository, seed_helper: true do repository.squash(user, squash_id, opts) end - context 'sparse checkout' do + context 'sparse checkout', :skip_gitaly_mock do let(:expected_files) { %w(files files/js files/js/application.js) } before do diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index d9ec28ab02e..9fbdd73ee0e 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -123,4 +123,53 @@ describe Gitlab::GitalyClient::OperationService do expect(subject.branch_created).to be(false) end end + + describe '#user_squash' do + let(:branch_name) { 'my-branch' } + let(:squash_id) { '1' } + let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' } + let(:commit_message) { 'Squash message' } + let(:request) do + Gitaly::UserSquashRequest.new( + repository: repository.gitaly_repository, + user: gitaly_user, + squash_id: squash_id.to_s, + branch: branch_name, + start_sha: start_sha, + end_sha: end_sha, + author: gitaly_user, + commit_message: commit_message + ) + end + let(:squash_sha) { 'f00' } + let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) } + + subject do + client.user_squash(user, squash_id, branch_name, start_sha, end_sha, user, commit_message) + end + + it 'sends a user_squash message and returns the squash sha' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_squash).with(request, kind_of(Hash)) + .and_return(response) + + expect(subject).to eq(squash_sha) + end + + context "when git_error is present" do + let(:response) do + Gitaly::UserSquashResponse.new(git_error: "something failed") + end + + it "throws a PreReceive exception" do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_squash).with(request, kind_of(Hash)) + .and_return(response) + + expect { subject }.to raise_error( + Gitlab::Git::Repository::GitError, "something failed") + end + end + end end diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 9dfd879a1bc..d076007e4bc 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -236,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do labels = project.issues.first.labels expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0)) + expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0) end end shared_examples 'restores group correctly' do |**results| it 'has group label' do expect(project.group.labels.size).to eq(results.fetch(:labels, 0)) + expect(project.group.labels.where(type: "GroupLabel").where.not(project_id: nil).count).to eq(0) end it 'has group milestone' do diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 0b8e97b8948..ebb6033f71e 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -63,14 +63,14 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'should mount configMap specification in the volume' do spec = subject.generate.spec - expect(spec.volumes.first.configMap['name']).to eq('values-content-configuration') + expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}") expect(spec.volumes.first.configMap['items'].first['key']).to eq('values') expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml') end end context 'without a configuration file' do - let(:app) { create(:clusters_applications_ingress, cluster: cluster) } + let(:app) { create(:clusters_applications_helm, cluster: cluster) } it_behaves_like 'helm pod' diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 9e405e9f736..03c185ddc07 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::Metrics do context 'prometheus metrics enabled in config' do before do - allow(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true) + allow(Gitlab::CurrentSettings).to receive(:prometheus_metrics_enabled).and_return(true) end context 'when metrics folder is present' do diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb new file mode 100644 index 00000000000..b49bc5c328c --- /dev/null +++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting::ActiveSupportSubscriber do + describe '#sql' do + it 'increments the number of executed SQL queries' do + transaction = double(:transaction) + + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) + + expect(transaction) + .to receive(:increment) + .at_least(:once) + + User.count + end + end +end diff --git a/spec/lib/gitlab/query_limiting/middleware_spec.rb b/spec/lib/gitlab/query_limiting/middleware_spec.rb new file mode 100644 index 00000000000..a04bcdecb4b --- /dev/null +++ b/spec/lib/gitlab/query_limiting/middleware_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting::Middleware do + describe '#call' do + it 'runs the application with query limiting in place' do + middleware = described_class.new(-> (env) { env }) + + expect_any_instance_of(Gitlab::QueryLimiting::Transaction) + .to receive(:act_upon_results) + + expect(middleware.call({ number: 10 })) + .to eq({ number: 10 }) + end + end + + describe '#action_name' do + let(:middleware) { described_class.new(-> (env) { env }) } + + context 'using a Rails request' do + it 'returns the name of the controller and action' do + env = { + described_class::CONTROLLER_KEY => double( + :controller, + action_name: 'show', + class: double(:class, name: 'UsersController'), + content_type: 'text/html' + ) + } + + expect(middleware.action_name(env)).to eq('UsersController#show') + end + + it 'includes the content type if this is not text/html' do + env = { + described_class::CONTROLLER_KEY => double( + :controller, + action_name: 'show', + class: double(:class, name: 'UsersController'), + content_type: 'application/json' + ) + } + + expect(middleware.action_name(env)) + .to eq('UsersController#show (application/json)') + end + end + + context 'using a Grape API request' do + it 'returns the name of the request method and endpoint path' do + env = { + described_class::ENDPOINT_KEY => double( + :endpoint, + route: double(:route, request_method: 'GET', path: '/foo') + ) + } + + expect(middleware.action_name(env)).to eq('GET /foo') + end + + it 'returns nil if the route can not be retrieved' do + endpoint = double(:endpoint) + env = { described_class::ENDPOINT_KEY => endpoint } + + allow(endpoint) + .to receive(:route) + .and_raise(RuntimeError) + + expect(middleware.action_name(env)).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb new file mode 100644 index 00000000000..b4231fcd0fa --- /dev/null +++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting::Transaction do + after do + Thread.current[described_class::THREAD_KEY] = nil + end + + describe '.current' do + it 'returns nil when there is no transaction' do + expect(described_class.current).to be_nil + end + + it 'returns the transaction when present' do + Thread.current[described_class::THREAD_KEY] = described_class.new + + expect(described_class.current).to be_an_instance_of(described_class) + end + end + + describe '.run' do + it 'runs a transaction and returns it and its return value' do + trans, ret = described_class.run do + 10 + end + + expect(trans).to be_an_instance_of(described_class) + expect(ret).to eq(10) + end + + it 'removes the transaction from the current thread upon completion' do + described_class.run do + 10 + end + + expect(Thread.current[described_class::THREAD_KEY]).to be_nil + end + end + + describe '#act_upon_results' do + context 'when the query threshold is not exceeded' do + it 'does nothing' do + trans = described_class.new + + expect(trans).not_to receive(:raise) + + trans.act_upon_results + end + end + + context 'when the query threshold is exceeded' do + let(:transaction) do + trans = described_class.new + trans.count = described_class::THRESHOLD + 1 + + trans + end + + it 'raises an error when this is enabled' do + expect { transaction.act_upon_results } + .to raise_error(described_class::ThresholdExceededError) + end + + it 'reports the error in Sentry if raising an error is disabled' do + expect(transaction) + .to receive(:raise_error?) + .and_return(false) + + expect(Raven) + .to receive(:capture_exception) + .with(an_instance_of(described_class::ThresholdExceededError)) + + transaction.act_upon_results + end + end + end + + describe '#increment' do + it 'increments the number of executed queries' do + transaction = described_class.new + + expect(transaction.count).to be_zero + + transaction.increment + + expect(transaction.count).to eq(1) + end + end + + describe '#raise_error?' do + it 'returns true in a test environment' do + transaction = described_class.new + + expect(transaction.raise_error?).to eq(true) + end + + it 'returns false in a production environment' do + transaction = described_class.new + + expect(Rails.env) + .to receive(:test?) + .and_return(false) + + expect(transaction.raise_error?).to eq(false) + end + end + + describe '#threshold_exceeded?' do + it 'returns false when the threshold is not exceeded' do + transaction = described_class.new + + expect(transaction.threshold_exceeded?).to eq(false) + end + + it 'returns true when the threshold is exceeded' do + transaction = described_class.new + transaction.count = described_class::THRESHOLD + 1 + + expect(transaction.threshold_exceeded?).to eq(true) + end + end + + describe '#error_message' do + it 'returns the error message to display when the threshold is exceeded' do + transaction = described_class.new + transaction.count = max = described_class::THRESHOLD + + expect(transaction.error_message).to eq( + "Too many SQL queries were executed: a maximum of #{max} " \ + "is allowed but #{max} SQL queries were executed" + ) + end + + it 'includes the action name in the error message when present' do + transaction = described_class.new + transaction.count = max = described_class::THRESHOLD + transaction.action = 'UsersController#show' + + expect(transaction.error_message).to eq( + "Too many SQL queries were executed in UsersController#show: " \ + "a maximum of #{max} is allowed but #{max} SQL queries were executed" + ) + end + end +end diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb new file mode 100644 index 00000000000..2eddab0b8c3 --- /dev/null +++ b/spec/lib/gitlab/query_limiting_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Gitlab::QueryLimiting do + describe '.enable?' do + it 'returns true in a test environment' do + expect(described_class.enable?).to eq(true) + end + + it 'returns true in a development environment' do + allow(Rails.env).to receive(:development?).and_return(true) + + expect(described_class.enable?).to eq(true) + end + + it 'returns true on GitLab.com' do + allow(Gitlab).to receive(:com?).and_return(true) + + expect(described_class.enable?).to eq(true) + end + + it 'returns true in a non GitLab.com' do + expect(Gitlab).to receive(:com?).and_return(false) + expect(Rails.env).to receive(:development?).and_return(false) + expect(Rails.env).to receive(:test?).and_return(false) + + expect(described_class.enable?).to eq(false) + end + end + + describe '.whitelist' do + it 'raises ArgumentError when an invalid issue URL is given' do + expect { described_class.whitelist('foo') } + .to raise_error(ArgumentError) + end + + context 'without a transaction' do + it 'does nothing' do + expect { described_class.whitelist('https://example.com') } + .not_to raise_error + end + end + + context 'with a transaction' do + let(:transaction) { Gitlab::QueryLimiting::Transaction.new } + + before do + allow(Gitlab::QueryLimiting::Transaction) + .to receive(:current) + .and_return(transaction) + end + + it 'does not increment the number of SQL queries executed in the block' do + before = transaction.count + + described_class.whitelist('https://example.com') + + 2.times do + User.count + end + + expect(transaction.count).to eq(before) + end + end + end +end diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb index 93d538141ce..c15e29774b6 100644 --- a/spec/lib/gitlab/ssh_public_key_spec.rb +++ b/spec/lib/gitlab/ssh_public_key_spec.rb @@ -37,6 +37,41 @@ describe Gitlab::SSHPublicKey, lib: true do end end + describe '.sanitize(key_content)' do + let(:content) { build(:key).key } + + context 'when key has blank space characters' do + it 'removes the extra blank space characters' do + unsanitized = content.insert(100, "\n") + .insert(40, "\r\n") + .insert(30, ' ') + + sanitized = described_class.sanitize(unsanitized) + _, body = sanitized.split + + expect(sanitized).not_to eq(unsanitized) + expect(body).not_to match(/\s/) + end + end + + context "when key doesn't have blank space characters" do + it "doesn't modify the content" do + sanitized = described_class.sanitize(content) + + expect(sanitized).to eq(content) + end + end + + context "when key is invalid" do + it 'returns the original content' do + unsanitized = "ssh-foo any content==" + sanitized = described_class.sanitize(unsanitized) + + expect(sanitized).to eq(unsanitized) + end + end + end + describe '#valid?' do subject { public_key } diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index b5f2a15ada3..0e9ecff25a6 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -103,9 +103,9 @@ describe Gitlab::UsageData do subject { described_class.features_usage_data_ce } it 'gathers feature usage data' do - expect(subject[:signup]).to eq(current_application_settings.allow_signup?) + expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?) expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled) - expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?) + expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?) expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled) expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?) expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled) @@ -129,7 +129,7 @@ describe Gitlab::UsageData do subject { described_class.license_usage_data } it "gathers license data" do - expect(subject[:uuid]).to eq(current_application_settings.uuid) + expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid) expect(subject[:version]).to eq(Gitlab::VERSION) expect(subject[:active_user_count]).to eq(User.active.count) expect(subject[:recorded_at]).to be_a(Time) diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index d85dac630b4..2c1146ceff5 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -57,6 +57,15 @@ describe Gitlab::VisibilityLevel do expect(described_class.allowed_levels) .to contain_exactly(described_class::PRIVATE, described_class::PUBLIC) end + + it 'returns all levels when no visibility level was set' do + allow(described_class) + .to receive_message_chain('current_application_settings.restricted_visibility_levels') + .and_return(nil) + + expect(described_class.allowed_levels) + .to contain_exactly(described_class::PRIVATE, described_class::INTERNAL, described_class::PUBLIC) + end end describe '.closest_allowed_level' do diff --git a/spec/migrations/remove_project_labels_group_id_spec.rb b/spec/migrations/remove_project_labels_group_id_spec.rb new file mode 100644 index 00000000000..d80d61af20b --- /dev/null +++ b/spec/migrations/remove_project_labels_group_id_spec.rb @@ -0,0 +1,21 @@ +# encoding: utf-8 + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_labels_group_id.rb') + +describe RemoveProjectLabelsGroupId, :delete do + let(:migration) { described_class.new } + let(:group) { create(:group) } + let!(:project_label) { create(:label, group_id: group.id) } + let!(:group_label) { create(:group_label) } + + describe '#up' do + it 'updates the project labels group ID' do + expect { migration.up }.to change { project_label.reload.group_id }.to(nil) + end + + it 'keeps the group labels group ID' do + expect { migration.up }.not_to change { group_label.reload.group_id } + end + end +end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 4cd9e3f4f1d..bf5703ac986 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -1,13 +1,6 @@ require 'spec_helper' describe Key, :mailer do - include Gitlab::CurrentSettings - - describe 'modules' do - subject { described_class } - it { is_expected.to include_module(Gitlab::CurrentSettings) } - end - describe "Associations" do it { is_expected.to belong_to(:user) } end @@ -79,16 +72,53 @@ describe Key, :mailer do expect(build(:key)).to be_valid end - it 'accepts a key with newline charecters after stripping them' do - key = build(:key) - key.key = key.key.insert(100, "\n") - key.key = key.key.insert(40, "\r\n") - expect(key).to be_valid - end - it 'rejects the unfingerprintable key (not a key)' do expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid end + + where(:factory, :chars, :expected_sections) do + [ + [:key, ["\n", "\r\n"], 3], + [:key, [' ', ' '], 3], + [:key_without_comment, [' ', ' '], 2] + ] + end + + with_them do + let!(:key) { create(factory) } + let!(:original_fingerprint) { key.fingerprint } + + it 'accepts a key with blank space characters after stripping them' do + modified_key = key.key.insert(100, chars.first).insert(40, chars.last) + _, content = modified_key.split + + key.update!(key: modified_key) + + expect(key).to be_valid + expect(key.key.split.size).to eq(expected_sections) + + expect(content).not_to match(/\s/) + expect(original_fingerprint).to eq(key.fingerprint) + end + end + end + + context 'validate size' do + where(:key_content, :result) do + [ + [Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false], + [Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false], + [Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true] + ] + end + + with_them do + it 'validates the size of the key' do + key = build(:key, key: key_content) + + expect(key.valid?).to eq(result) + end + end end context 'validate it meets key restrictions' do diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 6b7dbad128c..824cca66fb4 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -596,4 +596,24 @@ describe Namespace do end end end + + describe '#remove_exports' do + let(:legacy_project) { create(:project, :with_export, namespace: namespace) } + let(:hashed_project) { create(:project, :with_export, :hashed, namespace: namespace) } + let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') } + let(:legacy_export) { legacy_project.export_project_path } + let(:hashed_export) { hashed_project.export_project_path } + + it 'removes exports for legacy and hashed projects' do + allow(Gitlab::ImportExport).to receive(:storage_path) { export_path } + + expect(File.exist?(legacy_export)).to be_truthy + expect(File.exist?(hashed_export)).to be_truthy + + namespace.remove_exports! + + expect(File.exist?(legacy_export)).to be_falsy + expect(File.exist?(hashed_export)).to be_falsy + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 3d030927036..00dda7c9c60 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -17,8 +17,6 @@ describe Note do it { is_expected.to include_module(Participable) } it { is_expected.to include_module(Mentionable) } it { is_expected.to include_module(Awardable) } - - it { is_expected.to include_module(Gitlab::CurrentSettings) } end describe 'validation' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 31dcb543cbd..da940571bc1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -117,7 +117,6 @@ describe Project do it { is_expected.to include_module(Gitlab::ConfigHelper) } it { is_expected.to include_module(Gitlab::ShellAdapter) } it { is_expected.to include_module(Gitlab::VisibilityLevel) } - it { is_expected.to include_module(Gitlab::CurrentSettings) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } end @@ -2504,6 +2503,37 @@ describe Project do end end + describe '#remove_exports' do + let(:project) { create(:project, :with_export) } + + it 'removes the exports directory for the project' do + expect(File.exist?(project.export_path)).to be_truthy + + allow(FileUtils).to receive(:rm_rf).and_call_original + expect(FileUtils).to receive(:rm_rf).with(project.export_path).and_call_original + project.remove_exports + + expect(File.exist?(project.export_path)).to be_falsy + end + + it 'is a no-op when there is no namespace' do + export_path = project.export_path + project.update_column(:namespace_id, nil) + + expect(FileUtils).not_to receive(:rm_rf).with(export_path) + + project.remove_exports + + expect(File.exist?(export_path)).to be_truthy + end + + it 'is run when the project is destroyed' do + expect(project).to receive(:remove_exports).and_call_original + + project.destroy + end + end + describe '#forks_count' do it 'returns the number of forks' do project = build(:project) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 1102b1c9006..02a5ee54262 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -36,26 +36,49 @@ describe Repository do end describe '#branch_names_contains' do - subject { repository.branch_names_contains(sample_commit.id) } + shared_examples '#branch_names_contains' do + set(:project) { create(:project, :repository) } + let(:repository) { project.repository } - it { is_expected.to include('master') } - it { is_expected.not_to include('feature') } - it { is_expected.not_to include('fix') } + subject { repository.branch_names_contains(sample_commit.id) } - describe 'when storage is broken', :broken_storage do - it 'should raise a storage error' do - expect_to_raise_storage_error do - broken_repository.branch_names_contains(sample_commit.id) + it { is_expected.to include('master') } + it { is_expected.not_to include('feature') } + it { is_expected.not_to include('fix') } + + describe 'when storage is broken', :broken_storage do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.branch_names_contains(sample_commit.id) + end end end end + + context 'when gitaly is enabled' do + it_behaves_like '#branch_names_contains' + end + + context 'when gitaly is disabled', :skip_gitaly_mock do + it_behaves_like '#branch_names_contains' + end end describe '#tag_names_contains' do - subject { repository.tag_names_contains(sample_commit.id) } + shared_examples '#tag_names_contains' do + subject { repository.tag_names_contains(sample_commit.id) } - it { is_expected.to include('v1.1.0') } - it { is_expected.not_to include('v1.0.0') } + it { is_expected.to include('v1.1.0') } + it { is_expected.not_to include('v1.0.0') } + end + + context 'when gitaly is enabled' do + it_behaves_like '#tag_names_contains' + end + + context 'when gitaly is enabled', :skip_gitaly_mock do + it_behaves_like '#tag_names_contains' + end end describe 'tags_sorted_by' do @@ -959,19 +982,19 @@ describe Repository do end describe '#find_branch' do - it 'loads a branch with a fresh repo' do - expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original + context 'fresh_repo is true' do + it 'delegates the call to raw_repository' do + expect(repository.raw_repository).to receive(:find_branch).with('master', true) - 2.times do - expect(repository.find_branch('feature')).not_to be_nil + repository.find_branch('master', fresh_repo: true) end end - it 'loads a branch with a cached repo' do - expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original + context 'fresh_repo is false' do + it 'delegates the call to raw_repository' do + expect(repository.raw_repository).to receive(:find_branch).with('master', false) - 2.times do - expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil + repository.find_branch('master', fresh_repo: false) end end end diff --git a/spec/models/user_callout_spec.rb b/spec/models/user_callout_spec.rb new file mode 100644 index 00000000000..64ba17c81fe --- /dev/null +++ b/spec/models/user_callout_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe UserCallout do + let!(:callout) { create(:user_callout) } + + describe 'relationships' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + + it { is_expected.to validate_presence_of(:feature_name) } + it { is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id).ignoring_case_sensitivity } + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 594f23718da..18c91d4cffd 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,14 +1,12 @@ require 'spec_helper' describe User do - include Gitlab::CurrentSettings include ProjectForksHelper describe 'modules' do subject { described_class } it { is_expected.to include_module(Gitlab::ConfigHelper) } - it { is_expected.to include_module(Gitlab::CurrentSettings) } it { is_expected.to include_module(Referable) } it { is_expected.to include_module(Sortable) } it { is_expected.to include_module(TokenAuthenticatable) } @@ -560,7 +558,7 @@ describe User do stub_config_setting(default_can_create_group: true) expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true) - .and change { user.projects_limit }.to(current_application_settings.default_projects_limit) + .and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit) end end @@ -826,7 +824,7 @@ describe User do end end - context 'when current_application_settings.user_default_external is true' do + context 'when Gitlab::CurrentSettings.user_default_external is true' do before do stub_application_setting(user_default_external: true) end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 3c0b4728dc2..bb0034e3237 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -30,6 +30,21 @@ describe API::Groups do expect(json_response) .to satisfy_one { |group| group['name'] == group1.name } end + + it 'avoids N+1 queries' do + # Establish baseline + get api("/groups", admin) + + control = ActiveRecord::QueryRecorder.new do + get api("/groups", admin) + end + + create(:group) + + expect do + get api("/groups", admin) + end.not_to exceed_query_limit(control) + end end context "when authenticated as user" do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 97e7ffcd38e..f11cd638d96 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -2,8 +2,6 @@ require 'spec_helper' describe API::Projects do - include Gitlab::CurrentSettings - let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index 13e465e0b2d..5d99d9495f3 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' describe API::V3::Projects do - include Gitlab::CurrentSettings - let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index cb4c3e72aa0..e56d335a7d6 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -172,11 +172,32 @@ describe MergeRequests::BuildService do end end - context 'branch starts with external issue IID followed by a hyphen' do + context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do let(:source_branch) { '12345-fix-issue' } before do + allow(project).to receive(:external_issue_tracker).and_return(false) + allow(project).to receive(:issues_enabled?).and_return(false) + end + + it 'uses the title of the commit as the title of the merge request' do + expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first) + end + + it 'uses the description of the commit as the description of the merge request' do + commit_description = commit_1.safe_message.split(/\n+/, 2).last + + expect(merge_request.description).to eq("#{commit_description}") + end + end + + context 'branch starts with JIRA-formatted external issue IID followed by a hyphen' do + let(:source_branch) { 'EXMPL-12345-fix-issue' } + + before do allow(project).to receive(:external_issue_tracker).and_return(true) + allow(project).to receive(:issues_enabled?).and_return(false) + allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern) end it 'uses the title of the commit as the title of the merge request' do @@ -186,7 +207,7 @@ describe MergeRequests::BuildService do it 'uses the description of the commit as the description of the merge request and appends the closes text' do commit_description = commit_1.safe_message.split(/\n+/, 2).last - expect(merge_request.description).to eq("#{commit_description}\n\nCloses #12345") + expect(merge_request.description).to eq("#{commit_description}\n\nCloses EXMPL-12345") end end end @@ -252,19 +273,46 @@ describe MergeRequests::BuildService do end end - context 'branch starts with external issue IID followed by a hyphen' do + context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do let(:source_branch) { '12345-fix-issue' } before do - allow(project).to receive(:external_issue_tracker).and_return(true) + allow(project).to receive(:external_issue_tracker).and_return(false) + allow(project).to receive(:issues_enabled?).and_return(false) end it 'sets the title to the humanized branch title' do expect(merge_request.title).to eq('12345 fix issue') end + end + + context 'branch starts with JIRA-formatted external issue IID' do + let(:source_branch) { 'EXMPL-12345' } + + before do + allow(project).to receive(:external_issue_tracker).and_return(true) + allow(project).to receive(:issues_enabled?).and_return(false) + allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern) + end + + it 'sets the title to the humanized branch title' do + expect(merge_request.title).to eq('Resolve EXMPL-12345') + end it 'appends the closes text' do - expect(merge_request.description).to eq('Closes #12345') + expect(merge_request.description).to eq('Closes EXMPL-12345') + end + + context 'followed by hyphenated text' do + let(:source_branch) { 'EXMPL-12345-fix-issue' } + + it 'sets the title to the humanized branch title' do + expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"') + end + + it 'appends the closes text' do + expect(merge_request.description).to eq('Closes EXMPL-12345') + end end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 7c3374c6113..903aa0a5078 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -74,6 +74,14 @@ describe MergeRequests::RefreshService do expect(@fork_build_failed_todo).to be_done end + it 'reloads source branch MRs memoization' do + refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') + + expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }.to change { + refresh_service.instance_variable_get("@source_merge_requests").first.merge_request_diff + } + end + context 'when source branch ref does not exists' do before do DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch) @@ -392,37 +400,21 @@ describe MergeRequests::RefreshService do end it 'references the commit that caused the Work in Progress status' do - refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') - allow(refresh_service).to receive(:find_new_commits) - refresh_service.instance_variable_set("@commits", [ - double( - id: 'aaaaaaa', - sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e', - short_id: 'aaaaaaa', - title: 'Fix issue', - work_in_progress?: false - ), - double( - id: 'bbbbbbb', - sha: '498214de67004b1da3d820901307bed2a68a8ef6', - short_id: 'bbbbbbb', - title: 'fixup! Fix issue', - work_in_progress?: true, - to_reference: 'bbbbbbb' - ), - double( - id: 'ccccccc', - sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141', - short_id: 'ccccccc', - title: 'fixup! Fix issue', - work_in_progress?: true, - to_reference: 'ccccccc' - ) - ]) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip') - reload_mrs - expect(@merge_request.notes.last.note).to eq( - "marked as a **Work In Progress** from bbbbbbb" + wip_merge_request = create(:merge_request, + source_project: @project, + source_branch: 'wip', + target_branch: 'master', + target_project: @project) + + commits = wip_merge_request.commits + oldrev = commits.last.id + newrev = commits.first.id + wip_commit = wip_merge_request.commits.find(&:work_in_progress?) + + refresh_service.execute(oldrev, newrev, 'refs/heads/wip') + + expect(wip_merge_request.reload.notes.last.note).to eq( + "marked as a **Work In Progress** from #{wip_commit.id}" ) end diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index ab3aa18cf4e..5b5edc1aa0d 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -54,10 +54,11 @@ describe SystemNoteService do expect(note_lines[0]).to eq "added #{new_commits.size} commits" end - it 'adds a message line for each commit' do - new_commits.each_with_index do |commit, i| - # Skip the header - expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}" + it 'adds a message for each commit' do + decoded_note_content = HTMLEntities.new.decode(subject.note) + + new_commits.each do |commit| + expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>") end end end @@ -69,7 +70,7 @@ describe SystemNoteService do let(:old_commits) { [noteable.commits.last] } it 'includes the existing commit' do - expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`" + expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>") end end @@ -79,22 +80,16 @@ describe SystemNoteService do context 'with oldrev' do let(:oldrev) { noteable.commits[2].id } - it 'includes a commit range' do - expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}" - end - - it 'includes a commit count' do - expect(summary_line).to end_with " - 26 commits from branch `feature`" + it 'includes a commit range and count' do + expect(summary_line) + .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>") end end context 'without oldrev' do - it 'includes a commit range' do - expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}" - end - - it 'includes a commit count' do - expect(summary_line).to end_with " - 26 commits from branch `feature`" + it 'includes a commit range and count' do + expect(summary_line) + .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>") end end @@ -104,7 +99,7 @@ describe SystemNoteService do end it 'includes the project namespace' do - expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`" + expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>") end end end @@ -693,7 +688,7 @@ describe SystemNoteService do describe '.new_commit_summary' do it 'escapes HTML titles' do commit = double(title: '<pre>This is a test</pre>', short_id: '12345678') - escaped = '<pre>This is a test</pre>' + escaped = '<pre>This is a test</pre>' expect(described_class.new_commit_summary([commit])).to all(match(/- #{escaped}/)) end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index d12b2757427..ec4ec6f4038 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -190,6 +190,27 @@ module MarkdownMatchers expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4') end end + + # ColorFilter + matcher :parse_colors do + set_default_markdown_messages + + match do |actual| + color_chips = actual.css('code > span.gfm-color_chip > span') + + expect(color_chips.count).to eq(9) + + [ + '#F00', '#F00A', '#FF0000', '#FF0000AA', 'RGB(0,255,0)', + 'RGB(0%,100%,0%)', 'RGBA(0,255,0,0.7)', 'HSL(540,70%,50%)', + 'HSLA(540,70%,50%,0.7)' + ].each_with_index do |color, i| + parsed_color = Banzai::ColorParser.parse(color) + expect(color_chips[i]['style']).to match("background-color: #{parsed_color};") + expect(color_chips[i].parent.parent.content).to match(color) + end + end + end end # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index f9121cce985..52e47ae2d34 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -15,9 +15,7 @@ RSpec.configure do |config| # Track the maximum number of failures first_failure = Time.parse("2017-11-14 17:52:30") last_failure = Time.parse("2017-11-14 18:54:37") - failure_count = Gitlab::CurrentSettings - .current_application_settings - .circuitbreaker_failure_count_threshold + 1 + failure_count = Gitlab::CurrentSettings.circuitbreaker_failure_count_threshold + 1 cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}" Gitlab::Git::Storage.redis.with do |redis| diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb index 695152e2d4e..36b90fc68d6 100644 --- a/spec/support/stub_env.rb +++ b/spec/support/stub_env.rb @@ -1,7 +1,5 @@ # Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb module StubENV - include Gitlab::CurrentSettings - def stub_env(key_or_hash, value = nil) init_stub unless env_stubbed? diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb index 3d9705c9c05..e5c8ac6a004 100644 --- a/spec/support/unique_ip_check_shared_examples.rb +++ b/spec/support/unique_ip_check_shared_examples.rb @@ -9,7 +9,7 @@ shared_context 'unique ips sign in limit' do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') - current_application_settings.update!( + Gitlab::CurrentSettings.update!( unique_ips_limit_enabled: true, unique_ips_limit_time_window: 10000 ) @@ -34,7 +34,7 @@ end shared_examples 'user login operation with unique ip limit' do include_context 'unique ips sign in limit' do before do - current_application_settings.update!(unique_ips_limit_per_user: 1) + Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1) end it 'allows user authenticating from the same ip' do @@ -52,7 +52,7 @@ end shared_examples 'user login request with unique ip limit' do |success_status = 200| include_context 'unique ips sign in limit' do before do - current_application_settings.update!(unique_ips_limit_per_user: 1) + Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1) end it 'allows user authenticating from the same ip' do diff --git a/vendor/ingress/values.yaml b/vendor/ingress/values.yaml new file mode 100644 index 00000000000..cdb7da77e86 --- /dev/null +++ b/vendor/ingress/values.yaml @@ -0,0 +1,8 @@ +controller: + image: + tag: "0.10.2" + repository: "quay.io/kubernetes-ingress-controller/nginx-ingress-controller" + stats.enabled: true + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "10254" diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml index 5249449c7f8..db967514be7 100644 --- a/vendor/prometheus/values.yaml +++ b/vendor/prometheus/values.yaml @@ -2,7 +2,7 @@ alertmanager: enabled: false kubeStateMetrics: - enabled: false + enabled: true nodeExporter: enabled: false @@ -10,11 +10,15 @@ nodeExporter: pushgateway: enabled: false +server: + image: + tag: v2.1.0 + serverFiles: - alerts: "" - rules: "" + alerts: {} + rules: {} - prometheus.yml: |- + prometheus.yml: rule_files: - /etc/config/rules - /etc/config/alerts @@ -26,92 +30,108 @@ serverFiles: - job_name: kubernetes-cadvisor scheme: https tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt insecure_skip_verify: true - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token kubernetes_sd_configs: - - role: node - api_server: https://kubernetes.default.svc:443 - tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + - role: node relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: - - __meta_kubernetes_node_name - regex: "(.+)" - target_label: __metrics_path__ - replacement: "/api/v1/nodes/${1}/proxy/metrics/cadvisor" + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - target_label: __address__ + replacement: kubernetes.default.svc:443 + - source_labels: + - __meta_kubernetes_node_name + regex: "(.+)" + target_label: __metrics_path__ + replacement: "/api/v1/nodes/${1}/proxy/metrics/cadvisor" metric_relabel_configs: - - source_labels: - - pod_name - target_label: environment - regex: "(.+)-.+-.+" + - source_labels: + - pod_name + target_label: environment + regex: "(.+)-.+-.+" + - job_name: 'kubernetes-service-endpoints' + kubernetes_sd_configs: + - role: endpoints + relabel_configs: + - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape] + action: keep + regex: true + - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme] + action: replace + target_label: __scheme__ + regex: (https?) + - source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path] + action: replace + target_label: __metrics_path__ + regex: (.+) + - source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port] + action: replace + target_label: __address__ + regex: (.+)(?::\d+);(\d+) + replacement: $1:$2 + - action: labelmap + regex: __meta_kubernetes_service_label_(.+) + - source_labels: [__meta_kubernetes_namespace] + action: replace + target_label: kubernetes_namespace + - source_labels: [__meta_kubernetes_service_name] + action: replace + target_label: kubernetes_name - job_name: kubernetes-nodes scheme: https tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt insecure_skip_verify: true - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token kubernetes_sd_configs: - - role: node - api_server: https://kubernetes.default.svc:443 - tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + - role: node relabel_configs: - - action: labelmap - regex: __meta_kubernetes_node_label_(.+) - - target_label: __address__ - replacement: kubernetes.default.svc:443 - - source_labels: - - __meta_kubernetes_node_name - regex: "(.+)" - target_label: __metrics_path__ - replacement: "/api/v1/nodes/${1}/proxy/metrics" + - action: labelmap + regex: __meta_kubernetes_node_label_(.+) + - target_label: __address__ + replacement: kubernetes.default.svc:443 + - source_labels: + - __meta_kubernetes_node_name + regex: "(.+)" + target_label: __metrics_path__ + replacement: "/api/v1/nodes/${1}/proxy/metrics" metric_relabel_configs: - - source_labels: - - pod_name - target_label: environment - regex: "(.+)-.+-.+" + - source_labels: + - pod_name + target_label: environment + regex: "(.+)-.+-.+" - job_name: kubernetes-pods tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt insecure_skip_verify: true - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token kubernetes_sd_configs: - - role: pod - api_server: https://kubernetes.default.svc:443 - tls_config: - ca_file: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" - bearer_token_file: "/var/run/secrets/kubernetes.io/serviceaccount/token" + - role: pod relabel_configs: - - source_labels: - - __meta_kubernetes_pod_annotation_prometheus_io_scrape - action: keep - regex: 'true' - - source_labels: - - __meta_kubernetes_pod_annotation_prometheus_io_path - action: replace - target_label: __metrics_path__ - regex: "(.+)" - - source_labels: - - __address__ - - __meta_kubernetes_pod_annotation_prometheus_io_port - action: replace - regex: "([^:]+)(?::[0-9]+)?;([0-9]+)" - replacement: "$1:$2" - target_label: __address__ - - action: labelmap - regex: __meta_kubernetes_pod_label_(.+) - - source_labels: - - __meta_kubernetes_namespace - action: replace - target_label: kubernetes_namespace - - source_labels: - - __meta_kubernetes_pod_name - action: replace - target_label: kubernetes_pod_name + - source_labels: + - __meta_kubernetes_pod_annotation_prometheus_io_scrape + action: keep + regex: 'true' + - source_labels: + - __meta_kubernetes_pod_annotation_prometheus_io_path + action: replace + target_label: __metrics_path__ + regex: "(.+)" + - source_labels: + - __address__ + - __meta_kubernetes_pod_annotation_prometheus_io_port + action: replace + regex: "([^:]+)(?::[0-9]+)?;([0-9]+)" + replacement: "$1:$2" + target_label: __address__ + - action: labelmap + regex: __meta_kubernetes_pod_label_(.+) + - source_labels: + - __meta_kubernetes_namespace + action: replace + target_label: kubernetes_namespace + - source_labels: + - __meta_kubernetes_pod_name + action: replace + target_label: kubernetes_pod_name diff --git a/yarn.lock b/yarn.lock index d10a4372a40..e6d5f239d83 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3322,7 +3322,7 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.0, glob@~7.1.2: +glob@^7.1.0, glob@^7.1.2, glob@~7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: |