summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab/ci/rails.gitlab-ci.yml22
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml9
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/design_management/components/upload/button.vue2
-rw-r--r--app/assets/javascripts/global_search_input.js (renamed from app/assets/javascripts/search_autocomplete.js)207
-rw-r--r--app/assets/javascripts/main.js4
-rw-r--r--app/controllers/search_controller.rb15
-rw-r--r--app/helpers/search_helper.rb107
-rw-r--r--app/models/event.rb52
-rw-r--r--app/services/auto_merge/base_service.rb54
-rw-r--r--app/services/projects/create_service.rb20
-rw-r--r--app/views/layouts/_search.html.haml5
-rw-r--r--changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml5
-rw-r--r--changelogs/unreleased/213699-remove-search-results-autocomplete.yml5
-rw-r--r--changelogs/unreleased/curd-auto-merge-in-transaction.yml5
-rw-r--r--changelogs/unreleased/sh-update-workhorse-8-34-0.yml5
-rw-r--r--config/feature_categories.yml4
-rw-r--r--config/initializers/zz_metrics.rb1
-rw-r--r--config/routes.rb1
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md2
-rw-r--r--doc/ci/README.md6
-rw-r--r--doc/ci/migration/circleci.md332
-rw-r--r--doc/gitlab-basics/start-using-git.md5
-rw-r--r--doc/topics/git/numerous_undo_possibilities_in_git/index.md5
-rw-r--r--lib/gitlab/metrics/elasticsearch_rack_middleware.rb41
-rw-r--r--lib/gitlab/metrics/redis_rack_middleware.rb20
-rw-r--r--lib/gitlab/setup_helper.rb4
-rw-r--r--locale/gitlab.pot39
-rw-r--r--rubocop/cop/rspec/empty_line_after_shared_example.rb64
-rwxr-xr-xscripts/gitaly-test-build1
-rw-r--r--scripts/gitaly_test.rb27
-rw-r--r--spec/controllers/search_controller_spec.rb5
-rw-r--r--spec/fast_spec_helper.rb2
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap6
-rw-r--r--spec/frontend/fixtures/static/global_search_input.html (renamed from spec/frontend/fixtures/static/search_autocomplete.html)0
-rw-r--r--spec/helpers/search_helper_spec.rb93
-rw-r--r--spec/javascripts/global_search_input_spec.js (renamed from spec/javascripts/search_autocomplete_spec.js)20
-rw-r--r--spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb57
-rw-r--r--spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb91
-rw-r--r--spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb86
-rw-r--r--spec/services/auto_merge/base_service_spec.rb99
-rw-r--r--spec/services/projects/create_service_spec.rb45
-rw-r--r--spec/tooling/lib/tooling/test_file_finder_spec.rb111
-rwxr-xr-xtooling/bin/find_foss_tests29
-rw-r--r--tooling/lib/tooling/test_file_finder.rb78
46 files changed, 1077 insertions, 717 deletions
diff --git a/.gitignore b/.gitignore
index 9d9f2cdb475..abccf99229f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -92,3 +92,4 @@ webpack-dev-server.json
/.nvimrc
.solargraph.yml
apollo.config.js
+/tmp/matching_foss_tests.txt
diff --git a/.gitlab/ci/rails.gitlab-ci.yml b/.gitlab/ci/rails.gitlab-ci.yml
index b9c19b21ef3..fa468634c33 100644
--- a/.gitlab/ci/rails.gitlab-ci.yml
+++ b/.gitlab/ci/rails.gitlab-ci.yml
@@ -324,3 +324,25 @@ db:rollback geo:
- bundle exec rake geo:db:migrate
# EE: default refs (MRs, master, schedules) jobs #
##################################################
+
+##################################################
+# EE: Canonical MR pipelines
+rspec foss-impact:
+ extends:
+ - .rspec-base
+ - .as-if-foss
+ - .rails:rules:ee-mr-only
+ - .use-pg11
+ script:
+ - install_gitlab_gem
+ - run_timed_command "scripts/gitaly-test-build"
+ - run_timed_command "scripts/gitaly-test-spawn"
+ - source scripts/rspec_helpers.sh
+ - tooling/bin/find_foss_tests tmp/matching_foss_tests.txt
+ - rspec_simple_job "--tag ~quarantine --tag ~geo --tag ~level:migration $(cat tmp/matching_foss_tests.txt)"
+ artifacts:
+ expire_in: 7d
+ paths:
+ - tmp/matching_foss_tests.txt
+# EE: Merge Request pipelines
+##################################################
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index cd131d3f66a..db5e9f8acf4 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -446,6 +446,15 @@
- <<: *if-master-refs
changes: *code-backstage-patterns
+.rails:rules:ee-mr-only:
+ rules:
+ - <<: *if-not-ee
+ when: never
+ - <<: *if-security-merge-request
+ changes: *code-backstage-patterns
+ - <<: *if-dot-com-gitlab-org-merge-request
+ changes: *code-backstage-patterns
+
.rails:rules:downtime_check:
rules:
- <<: *if-merge-request
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 9abb305dd99..a35ebc6eaa7 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.33.0
+8.34.0
diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue
index e3c5e369170..68555104a3c 100644
--- a/app/assets/javascripts/design_management/components/upload/button.vue
+++ b/app/assets/javascripts/design_management/components/upload/button.vue
@@ -41,7 +41,7 @@ export default {
variant="success"
@click="openFileUpload"
>
- {{ s__('DesignManagement|Add designs') }}
+ {{ s__('DesignManagement|Upload designs') }}
<gl-loading-icon v-if="isSaving" inline class="ml-1" />
</gl-deprecated-button>
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/global_search_input.js
index 05e0b9e7089..a7c121259d4 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/global_search_input.js
@@ -1,10 +1,8 @@
/* eslint-disable no-return-assign, consistent-return, class-methods-use-this */
import $ from 'jquery';
-import { escape, throttle } from 'lodash';
+import { throttle } from 'lodash';
import { s__, __, sprintf } from '~/locale';
-import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
-import axios from './lib/utils/axios_utils';
import {
isInGroupsPage,
isInProjectPage,
@@ -67,15 +65,11 @@ function setSearchOptions() {
}
}
-export class SearchAutocomplete {
- constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
+export class GlobalSearchInput {
+ constructor({ wrap } = {}) {
setSearchOptions();
this.bindEventContext();
this.wrap = wrap || $('.search');
- this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
- this.autocompletePath = autocompletePath || this.optsEl.data('autocompletePath');
- this.projectId = projectId || (this.optsEl.data('autocompleteProjectId') || '');
- this.projectRef = projectRef || (this.optsEl.data('autocompleteProjectRef') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownToggle = this.wrap.find('.js-dropdown-search-toggle');
this.dropdownMenu = this.dropdown.find('.dropdown-menu');
@@ -92,7 +86,7 @@ export class SearchAutocomplete {
// Only when user is logged in
if (gon.current_user_id) {
- this.createAutocomplete();
+ this.createGlobalSearchInput();
}
this.bindEvents();
@@ -117,7 +111,7 @@ export class SearchAutocomplete {
return (this.originalState = this.serializeState());
}
- createAutocomplete() {
+ createGlobalSearchInput() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
@@ -149,116 +143,17 @@ export class SearchAutocomplete {
if (glDropdownInstance) {
glDropdownInstance.filter.options.callback(contents);
}
- this.enableAutocomplete();
+ this.enableDropdown();
}
return;
}
- // Prevent multiple ajax calls
- if (this.loadingSuggestions) {
- return;
- }
-
- this.loadingSuggestions = true;
-
- return axios
- .get(this.autocompletePath, {
- params: {
- project_id: this.projectId,
- project_ref: this.projectRef,
- term,
- },
- })
- .then(response => {
- const options = this.scopedSearchOptions(term);
-
- // List results
- let lastCategory = null;
- for (let i = 0, len = response.data.length; i < len; i += 1) {
- const suggestion = response.data[i];
- // Add group header before list each group
- if (lastCategory !== suggestion.category) {
- options.push({ type: 'separator' });
- options.push({
- type: 'header',
- content: suggestion.category,
- });
- lastCategory = suggestion.category;
- }
-
- // Add the suggestion
- options.push({
- id: `${suggestion.category.toLowerCase()}-${suggestion.id}`,
- icon: this.getAvatar(suggestion),
- category: suggestion.category,
- text: suggestion.label,
- url: suggestion.url,
- });
- }
-
- callback(options);
-
- this.loadingSuggestions = false;
- this.highlightFirstRow();
- this.setScrollFade();
- })
- .catch(() => {
- this.loadingSuggestions = false;
- });
- }
-
- getCategoryContents() {
- const userName = gon.current_username;
- const { projectOptions, groupOptions, dashboardOptions } = gl;
-
- // Get options
- let options;
- if (isInProjectPage() && projectOptions) {
- options = projectOptions[getProjectSlug()];
- } else if (isInGroupsPage() && groupOptions) {
- options = groupOptions[getGroupSlug()];
- } else if (dashboardOptions) {
- options = dashboardOptions;
- }
-
- const { issuesPath, mrPath, name, issuesDisabled } = options;
- const baseItems = [];
-
- if (name) {
- baseItems.push({
- type: 'header',
- content: `${name}`,
- });
- }
+ const options = this.scopedSearchOptions(term);
- const issueItems = [
- {
- text: s__('SearchAutocomplete|Issues assigned to me'),
- url: `${issuesPath}/?assignee_username=${userName}`,
- },
- {
- text: s__("SearchAutocomplete|Issues I've created"),
- url: `${issuesPath}/?author_username=${userName}`,
- },
- ];
- const mergeRequestItems = [
- {
- text: s__('SearchAutocomplete|Merge requests assigned to me'),
- url: `${mrPath}/?assignee_username=${userName}`,
- },
- {
- text: s__("SearchAutocomplete|Merge requests I've created"),
- url: `${mrPath}/?author_username=${userName}`,
- },
- ];
+ callback(options);
- let items;
- if (issuesDisabled) {
- items = baseItems.concat(mergeRequestItems);
- } else {
- items = baseItems.concat(...issueItems, ...mergeRequestItems);
- }
- return items;
+ this.highlightFirstRow();
+ this.setScrollFade();
}
// Add option to proceed with the search for each
@@ -343,7 +238,7 @@ export class SearchAutocomplete {
});
}
- enableAutocomplete() {
+ enableDropdown() {
this.setScrollFade();
// No need to enable anything if user is not logged in
@@ -360,7 +255,7 @@ export class SearchAutocomplete {
}
onSearchInputChange() {
- this.enableAutocomplete();
+ this.enableDropdown();
}
onSearchInputKeyUp(e) {
@@ -369,7 +264,7 @@ export class SearchAutocomplete {
this.restoreOriginalState();
break;
case KEYCODE.ENTER:
- this.disableAutocomplete();
+ this.disableDropdown();
break;
default:
}
@@ -422,7 +317,7 @@ export class SearchAutocomplete {
return results;
}
- disableAutocomplete() {
+ disableDropdown() {
if (!this.searchInput.hasClass('js-autocomplete-disabled') && this.dropdown.hasClass('show')) {
this.searchInput.addClass('js-autocomplete-disabled');
this.dropdownToggle.dropdown('toggle');
@@ -438,16 +333,8 @@ export class SearchAutocomplete {
onClick(item, $el, e) {
if (window.location.pathname.indexOf(item.url) !== -1) {
if (!e.metaKey) e.preventDefault();
- /* eslint-disable-next-line @gitlab/require-i18n-strings */
- if (item.category === 'Projects') {
- this.projectInputEl.val(item.id);
- }
- // eslint-disable-next-line @gitlab/require-i18n-strings
- if (item.category === 'Groups') {
- this.groupInputEl.val(item.id);
- }
$el.removeClass('is-active');
- this.disableAutocomplete();
+ this.disableDropdown();
return this.searchInput.val('').focus();
}
}
@@ -456,20 +343,58 @@ export class SearchAutocomplete {
this.searchInput.data('glDropdown').highlightRowAtIndex(null, 0);
}
- getAvatar(item) {
- if (!Object.hasOwnProperty.call(item, 'avatar_url')) {
- return false;
+ getCategoryContents() {
+ const userName = gon.current_username;
+ const { projectOptions, groupOptions, dashboardOptions } = gl;
+
+ // Get options
+ let options;
+ if (isInProjectPage() && projectOptions) {
+ options = projectOptions[getProjectSlug()];
+ } else if (isInGroupsPage() && groupOptions) {
+ options = groupOptions[getGroupSlug()];
+ } else if (dashboardOptions) {
+ options = dashboardOptions;
}
- const { label, id } = item;
- const avatarUrl = item.avatar_url;
- const avatar = avatarUrl
- ? `<img class="search-item-avatar" src="${avatarUrl}" />`
- : `<div class="s16 avatar identicon ${getIdenticonBackgroundClass(id)}">${getIdenticonTitle(
- escape(label),
- )}</div>`;
+ const { issuesPath, mrPath, name, issuesDisabled } = options;
+ const baseItems = [];
+
+ if (name) {
+ baseItems.push({
+ type: 'header',
+ content: `${name}`,
+ });
+ }
- return avatar;
+ const issueItems = [
+ {
+ text: s__('SearchAutocomplete|Issues assigned to me'),
+ url: `${issuesPath}/?assignee_username=${userName}`,
+ },
+ {
+ text: s__("SearchAutocomplete|Issues I've created"),
+ url: `${issuesPath}/?author_username=${userName}`,
+ },
+ ];
+ const mergeRequestItems = [
+ {
+ text: s__('SearchAutocomplete|Merge requests assigned to me'),
+ url: `${mrPath}/?assignee_username=${userName}`,
+ },
+ {
+ text: s__("SearchAutocomplete|Merge requests I've created"),
+ url: `${mrPath}/?author_username=${userName}`,
+ },
+ ];
+
+ let items;
+ if (issuesDisabled) {
+ items = baseItems.concat(mergeRequestItems);
+ } else {
+ items = baseItems.concat(...issueItems, ...mergeRequestItems);
+ }
+ return items;
}
isScrolledUp() {
@@ -495,6 +420,6 @@ export class SearchAutocomplete {
}
}
-export default function initSearchAutocomplete(opts) {
- return new SearchAutocomplete(opts);
+export default function initGlobalSearchInput(opts) {
+ return new GlobalSearchInput(opts);
}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 6e3066f55d5..5f5fd790f67 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -32,7 +32,7 @@ import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initUsagePingConsent from './usage_ping_consent';
import initPerformanceBar from './performance_bar';
-import initSearchAutocomplete from './search_autocomplete';
+import initGlobalSearchInput from './global_search_input';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@@ -110,7 +110,7 @@ function deferredInitialisation() {
initFrequentItemDropdowns();
initPersistentUserCallouts();
- if (document.querySelector('.search')) initSearchAutocomplete();
+ if (document.querySelector('.search')) initGlobalSearchInput();
addSelectOnFocusBehaviour('.js-select-on-focus');
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index ff6d9350a5c..217f08dd648 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -51,21 +51,6 @@ class SearchController < ApplicationController
render json: { count: count }
end
- # rubocop: disable CodeReuse/ActiveRecord
- def autocomplete
- term = params[:term]
-
- if params[:project_id].present?
- @project = Project.find_by(id: params[:project_id])
- @project = nil unless can?(current_user, :read_project, @project)
- end
-
- @ref = params[:project_ref] if params[:project_ref].present?
-
- render json: search_autocomplete_opts(term).to_json
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
private
def preload_method
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 5ad65c59a2e..4e3b6aad8cc 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -3,28 +3,6 @@
module SearchHelper
SEARCH_PERMITTED_PARAMS = [:search, :scope, :project_id, :group_id, :repository_ref, :snippets].freeze
- def search_autocomplete_opts(term)
- return unless current_user
-
- resources_results = [
- groups_autocomplete(term),
- projects_autocomplete(term)
- ].flatten
-
- search_pattern = Regexp.new(Regexp.escape(term), "i")
-
- generic_results = project_autocomplete + default_autocomplete + help_autocomplete
- generic_results.concat(default_autocomplete_admin) if current_user.admin?
- generic_results.select! { |result| result[:label] =~ search_pattern }
-
- [
- resources_results,
- generic_results
- ].flatten.uniq do |item|
- item[:label]
- end
- end
-
def search_entries_info(collection, scope, term)
return if collection.to_a.empty?
@@ -95,91 +73,6 @@ module SearchHelper
private
- # Autocomplete results for various settings pages
- def default_autocomplete
- [
- { category: "Settings", label: _("User settings"), url: profile_path },
- { category: "Settings", label: _("SSH Keys"), url: profile_keys_path },
- { category: "Settings", label: _("Dashboard"), url: root_path }
- ]
- end
-
- # Autocomplete results for settings pages, for admins
- def default_autocomplete_admin
- [
- { category: "Settings", label: _("Admin Section"), url: admin_root_path }
- ]
- end
-
- # Autocomplete results for internal help pages
- def help_autocomplete
- [
- { category: "Help", label: _("API Help"), url: help_page_path("api/README") },
- { category: "Help", label: _("Markdown Help"), url: help_page_path("user/markdown") },
- { category: "Help", label: _("Permissions Help"), url: help_page_path("user/permissions") },
- { category: "Help", label: _("Public Access Help"), url: help_page_path("public_access/public_access") },
- { category: "Help", label: _("Rake Tasks Help"), url: help_page_path("raketasks/README") },
- { category: "Help", label: _("SSH Keys Help"), url: help_page_path("ssh/README") },
- { category: "Help", label: _("System Hooks Help"), url: help_page_path("system_hooks/system_hooks") },
- { category: "Help", label: _("Webhooks Help"), url: help_page_path("user/project/integrations/webhooks") },
- { category: "Help", label: _("Workflow Help"), url: help_page_path("workflow/README") }
- ]
- end
-
- # Autocomplete results for the current project, if it's defined
- def project_autocomplete
- if @project && @project.repository.root_ref
- ref = @ref || @project.repository.root_ref
-
- [
- { category: "In this project", label: _("Files"), url: project_tree_path(@project, ref) },
- { category: "In this project", label: _("Commits"), url: project_commits_path(@project, ref) },
- { category: "In this project", label: _("Network"), url: project_network_path(@project, ref) },
- { category: "In this project", label: _("Graph"), url: project_graph_path(@project, ref) },
- { category: "In this project", label: _("Issues"), url: project_issues_path(@project) },
- { category: "In this project", label: _("Merge Requests"), url: project_merge_requests_path(@project) },
- { category: "In this project", label: _("Milestones"), url: project_milestones_path(@project) },
- { category: "In this project", label: _("Snippets"), url: project_snippets_path(@project) },
- { category: "In this project", label: _("Members"), url: project_project_members_path(@project) },
- { category: "In this project", label: _("Wiki"), url: project_wikis_path(@project) }
- ]
- else
- []
- end
- end
-
- # Autocomplete results for the current user's groups
- # rubocop: disable CodeReuse/ActiveRecord
- def groups_autocomplete(term, limit = 5)
- current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
- {
- category: "Groups",
- id: group.id,
- label: "#{search_result_sanitize(group.full_name)}",
- url: group_path(group),
- avatar_url: group.avatar_url || ''
- }
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # Autocomplete results for the current user's projects
- # rubocop: disable CodeReuse/ActiveRecord
- def projects_autocomplete(term, limit = 5)
- current_user.authorized_projects.order_id_desc.search_by_title(term)
- .sorted_by_stars_desc.non_archived.limit(limit).map do |p|
- {
- category: "Projects",
- id: p.id,
- value: "#{search_result_sanitize(p.name)}",
- label: "#{search_result_sanitize(p.full_name)}",
- url: project_path(p),
- avatar_url: p.avatar_url || ''
- }
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def search_result_sanitize(str)
Sanitize.clean(str)
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 926c3fe6a25..e404ddc22eb 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -149,7 +149,9 @@ class Event < ApplicationRecord
def visible_to_user?(user = nil)
return false unless capability.present?
- Ability.allowed?(user, capability, permission_object)
+ capability.all? do |rule|
+ Ability.allowed?(user, rule, permission_object)
+ end
end
def resource_parent
@@ -361,34 +363,30 @@ class Event < ApplicationRecord
protected
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- #
- # TODO Refactor this method so we no longer need to disable the above cops
- # https://gitlab.com/gitlab-org/gitlab/-/issues/216879.
def capability
@capability ||= begin
- if push_action? || commit_note?
- :download_code
- elsif membership_changed? || created_project_action?
- :read_project
- elsif issue? || issue_note?
- :read_issue
- elsif merge_request? || merge_request_note?
- :read_merge_request
- elsif personal_snippet_note? || project_snippet_note?
- :read_snippet
- elsif milestone?
- :read_milestone
- elsif wiki_page?
- :read_wiki
- elsif design_note? || design?
- :read_design
- end
- end
- end
- # rubocop:enable Metrics/CyclomaticComplexity
- # rubocop:enable Metrics/PerceivedComplexity
+ capabilities.flat_map do |ability, syms|
+ if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
+ [ability]
+ else
+ []
+ end
+ end
+ end
+ end
+
+ def capabilities
+ {
+ download_code: %i[push_action? commit_note?],
+ read_project: %i[membership_changed? created_project_action?],
+ read_issue: %i[issue? issue_note?],
+ read_merge_request: %i[merge_request? merge_request_note?],
+ read_snippet: %i[personal_snippet_note? project_snippet_note?],
+ read_milestone: %i[milestone?],
+ read_wiki: %i[wiki_page?],
+ read_design: %i[design_note? design?]
+ }
+ end
private
diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb
index 1de2f31f87c..c4109765a1c 100644
--- a/app/services/auto_merge/base_service.rb
+++ b/app/services/auto_merge/base_service.rb
@@ -6,19 +6,18 @@ module AutoMerge
include MergeRequests::AssignsMergeParams
def execute(merge_request)
- assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
-
- merge_request.auto_merge_enabled = true
- merge_request.merge_user = current_user
-
- return :failed unless merge_request.save
-
- yield if block_given?
+ ActiveRecord::Base.transaction do
+ register_auto_merge_parameters!(merge_request)
+ yield if block_given?
+ end
# Notify the event that auto merge is enabled or merge param is updated
AutoMergeProcessWorker.perform_async(merge_request.id)
strategy.to_sym
+ rescue => e
+ track_exception(e, merge_request)
+ :failed
end
def update(merge_request)
@@ -30,23 +29,27 @@ module AutoMerge
end
def cancel(merge_request)
- if clear_auto_merge_parameters(merge_request)
+ ActiveRecord::Base.transaction do
+ clear_auto_merge_parameters!(merge_request)
yield if block_given?
-
- success
- else
- error("Can't cancel the automatic merge", 406)
end
+
+ success
+ rescue => e
+ track_exception(e, merge_request)
+ error("Can't cancel the automatic merge", 406)
end
def abort(merge_request, reason)
- if clear_auto_merge_parameters(merge_request)
+ ActiveRecord::Base.transaction do
+ clear_auto_merge_parameters!(merge_request)
yield if block_given?
-
- success
- else
- error("Can't abort the automatic merge", 406)
end
+
+ success
+ rescue => e
+ track_exception(e, merge_request)
+ error("Can't abort the automatic merge", 406)
end
def available_for?(merge_request)
@@ -65,7 +68,14 @@ module AutoMerge
end
end
- def clear_auto_merge_parameters(merge_request)
+ def register_auto_merge_parameters!(merge_request)
+ assign_allowed_merge_params(merge_request, params.merge(auto_merge_strategy: strategy))
+ merge_request.auto_merge_enabled = true
+ merge_request.merge_user = current_user
+ merge_request.save!
+ end
+
+ def clear_auto_merge_parameters!(merge_request)
merge_request.auto_merge_enabled = false
merge_request.merge_user = nil
@@ -76,7 +86,11 @@ module AutoMerge
'auto_merge_strategy'
)
- merge_request.save
+ merge_request.save!
+ end
+
+ def track_exception(error, merge_request)
+ Gitlab::ErrorTracking.track_exception(error, merge_request_id: merge_request&.id)
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 8b3ecd32ca3..bffd443c49f 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -150,7 +150,7 @@ module Projects
if @project.save
unless @project.gitlab_project_import?
- create_services_from_active_templates(@project)
+ create_services_from_active_instances_or_templates(@project)
@project.create_labels
end
@@ -175,15 +175,6 @@ module Projects
@project
end
- # rubocop: disable CodeReuse/ActiveRecord
- def create_services_from_active_templates(project)
- Service.where(template: true, active: true).each do |template|
- service = Service.build_from_integration(project.id, template)
- service.save!
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def create_prometheus_service
service = @project.find_or_initialize_service(::PrometheusService.to_param)
@@ -225,6 +216,15 @@ module Projects
private
+ # rubocop: disable CodeReuse/ActiveRecord
+ def create_services_from_active_instances_or_templates(project)
+ Service.active.where(instance: true).or(Service.active.where(template: true)).group_by(&:type).each do |type, records|
+ service = records.find(&:instance?) || records.find(&:template?)
+ Service.build_from_integration(project.id, service).save!
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
def project_namespace
@project_namespace ||= Namespace.find_by_id(@params[:namespace_id]) || current_user.namespace
end
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 81fe0798bd1..97d00bce11b 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -2,7 +2,7 @@
= form_tag search_path, method: :get, class: 'form-inline' do |f|
.search-input-container
.search-input-wrap
- .dropdown{ data: { url: search_autocomplete_path } }
+ .dropdown
= search_field_tag 'search', nil, placeholder: _('Search or jump to…'),
class: 'search-input dropdown-menu-toggle no-outline js-search-dashboard-options',
spellcheck: false,
@@ -37,6 +37,3 @@
-# workaround for non-JS feature specs, see spec/support/helpers/search_helpers.rb
- if ENV['RAILS_ENV'] == 'test'
%noscript= button_tag 'Search'
- .search-autocomplete-opts.hide{ :'data-autocomplete-path' => search_autocomplete_path,
- :'data-autocomplete-project-id' => search_context.project.try(:id),
- :'data-autocomplete-project-ref' => search_context.ref }
diff --git a/changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml b/changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml
new file mode 100644
index 00000000000..cb172caf997
--- /dev/null
+++ b/changelogs/unreleased/211828-placement-of-add-designs-button-could-be-confusing-with-the-additi.yml
@@ -0,0 +1,5 @@
+---
+title: Rename Add Designs button
+merge_request: 33491
+author:
+type: changed
diff --git a/changelogs/unreleased/213699-remove-search-results-autocomplete.yml b/changelogs/unreleased/213699-remove-search-results-autocomplete.yml
new file mode 100644
index 00000000000..539d4695658
--- /dev/null
+++ b/changelogs/unreleased/213699-remove-search-results-autocomplete.yml
@@ -0,0 +1,5 @@
+---
+title: Remove all search autocomplete for groups/projects/other
+merge_request: 31187
+author:
+type: removed
diff --git a/changelogs/unreleased/curd-auto-merge-in-transaction.yml b/changelogs/unreleased/curd-auto-merge-in-transaction.yml
new file mode 100644
index 00000000000..9a9cfbea1c7
--- /dev/null
+++ b/changelogs/unreleased/curd-auto-merge-in-transaction.yml
@@ -0,0 +1,5 @@
+---
+title: Wrap auto merge parameters update in database transaction
+merge_request: 33471
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-update-workhorse-8-34-0.yml b/changelogs/unreleased/sh-update-workhorse-8-34-0.yml
new file mode 100644
index 00000000000..5ecb3840463
--- /dev/null
+++ b/changelogs/unreleased/sh-update-workhorse-8-34-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update GitLab Workhorse to v8.34.0
+merge_request: 33543
+author:
+type: fixed
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 010d3d14fcb..7cbc90497a4 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -43,6 +43,7 @@
- digital_experience_management
- disaster_recovery
- dynamic_application_security_testing
+- editor_extension
- epics
- error_tracking
- feature_flags
@@ -52,6 +53,7 @@
- geo_replication
- git_lfs
- gitaly
+- gitlab_docs
- gitlab_handbook
- gitter
- global_search
@@ -82,6 +84,7 @@
- pages
- pki_management
- planning_analytics
+- product_analytics
- quality_management
- release_evidence
- release_orchestration
@@ -100,7 +103,6 @@
- source_code_management
- static_application_security_testing
- static_site_editor
-- status_page
- subgroups
- templates
- time_tracking
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index 9636440926b..7e675e478cf 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -148,6 +148,7 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
config.middleware.use(Gitlab::Metrics::RackMiddleware)
config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
config.middleware.use(Gitlab::Metrics::RedisRackMiddleware)
+ config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware)
end
Sidekiq.configure_server do |config|
diff --git a/config/routes.rb b/config/routes.rb
index bfe8172a88e..72d9c531017 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -58,7 +58,6 @@ Rails.application.routes.draw do
# Search
get 'search' => 'search#show'
- get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
get 'search/count' => 'search#count', as: :search_count
# JSON Web Token
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index fdd64c20c70..695de61f9a7 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -94,6 +94,8 @@ The following metrics are available:
| `http_request_duration_seconds` | Histogram | 9.4 | HTTP response time from rack middleware | `method`, `status` |
| `http_redis_requests_duration_seconds` | Histogram | 13.1 | Redis requests duration during web transactions | `controller`, `action` |
| `http_redis_requests_total` | Counter | 13.1 | Redis requests count during web transactions | `controller`, `action` |
+| `http_elasticsearch_requests_duration_seconds` **(STARTER)** | Histogram | 13.1 | Elasticsearch requests duration during web transactions | `controller`, `action` |
+| `http_elasticsearch_requests_total` **(STARTER)** | Counter | 13.1 | Elasticsearch requests count during web transactions | `controller`, `action` |
| `pipelines_created_total` | Counter | 9.4 | Counter of pipelines created | |
| `rack_uncaught_errors_total` | Counter | 9.4 | Rack connections handling uncaught errors count | |
| `user_session_logins_total` | Counter | 9.4 | Counter of how many users have logged in | |
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 61f00d2e0fd..806d4dba3cd 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -60,8 +60,10 @@ the following documents:
- [GitLab CI/CD basic workflow](introduction/index.md#basic-cicd-workflow).
- [Step-by-step guide for writing `.gitlab-ci.yml` for the first time](../user/project/pages/getting_started_part_four.md).
-If you're coming over from Jenkins, you can also check out our handy [reference](jenkins/index.md)
-for converting your pipelines.
+If you're migrating from another CI/CD tool, check out our handy references:
+
+- [Migrating from CircleCI](migration/circleci.md)
+- [Migrating from Jenkins](jenkins/index.md)
You can also get started by using one of the
[`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates)
diff --git a/doc/ci/migration/circleci.md b/doc/ci/migration/circleci.md
new file mode 100644
index 00000000000..f6868abc334
--- /dev/null
+++ b/doc/ci/migration/circleci.md
@@ -0,0 +1,332 @@
+---
+comments: false
+type: index, howto
+---
+
+# Migrating from CircleCI
+
+If you are currently using CircleCI, you can migrate your CI/CD pipelines to [GitLab CI/CD](../introduction/index.md),
+and start making use of all its powerful features. Check out our
+[CircleCI vs GitLab](https://about.gitlab.com/devops-tools/circle-ci-vs-gitlab.html)
+comparison to see what's different.
+
+We have collected several resources that you may find useful before starting to migrate.
+
+The [Quick Start Guide](../quick_start/README.md) is a good overview of how GitLab CI/CD works. You may also be interested in [Auto DevOps](../../topics/autodevops/index.md) which can be used to build, test, and deploy your applications with little to no configuration needed at all.
+
+For advanced CI/CD teams, [custom project templates](../../user/admin_area/custom_project_templates.md) can enable the reuse of pipeline configurations.
+
+If you have questions that are not answered here, the [GitLab community forum](https://forum.gitlab.com/) can be a great resource.
+
+## `config.yml` vs `gitlab-ci.yml`
+
+CircleCI's `config.yml` configuration file defines scripts, jobs, and workflows (known as "stages" in GitLab). In GitLab, a similar approach is used with a `.gitlab-ci.yml` file in the root directory of your repository.
+
+### Jobs
+
+In CircleCI, jobs are a collection of steps to perform a specific task. In GitLab, [jobs](../yaml/README.md#introduction) are also a fundamental element in the configuration file. The `checkout` parameter is not necessary in GitLab CI/CD as the repository is automatically fetched.
+
+CircleCI example job definition:
+
+```yaml
+jobs:
+ job1:
+ steps:
+ - checkout
+ - run: "execute-script-for-job1"
+```
+
+Example of the same job definition in GitLab CI/CD:
+
+``` yaml
+job1:
+ script: "execute-script-for-job1"
+```
+
+### Docker image definition
+
+CircleCI defines images at the job level, which is also supported by GitLab CI/CD. Additionally, GitLab CI/CD supports setting this globally to be used by all jobs that don't have `image` defined.
+
+CircleCI example image definition:
+
+```yaml
+jobs:
+ job1:
+ docker:
+ - image: ruby:2.6
+```
+
+Example of the same image definition in GitLab CI/CD:
+
+```yaml
+job1:
+ image: ruby:2.6
+```
+
+### Workflows
+
+CircleCI determines the run order for jobs with `workflows`. This is also used to determine concurrent, sequential, scheduled, or manual runs. The equivalent function in GitLab CI/CD is called [stages](../yaml/README.md#stages). Jobs on the same stage run in parallel, and only run after previous stages complete. Execution of the next stage is skipped when a job fails by default, but this can be allowed to continue even [after a failed job](../yaml/README.md#allow_failure).
+
+See [the Pipeline Architecture Overview](../pipelines/pipeline_architectures.md) for guidance on different types of pipelines that you can use. Pipelines can be tailored to meet your needs, such as for a large complex project or a monorepo with independent defined components.
+
+#### Parallel and sequential job execution
+
+The following examples show how jobs can run in parallel, or sequentially:
+
+1. `job1` and `job2` run in parallel (in the `build` stage for GitLab CI/CD).
+1. `job3` runs only after `job1` and `job2` complete successfully (in the `test` stage).
+1. `job4` runs only after `job3` completes successfully (in the `deploy` stage).
+
+CircleCI example with `workflows`:
+
+```yaml
+version: 2
+jobs:
+ job1:
+ steps:
+ - checkout
+ - run: make build dependencies
+ job2:
+ steps:
+ - run: make build artifacts
+ job3:
+ steps:
+ - run: make test
+ job4:
+ steps:
+ - run: make deploy
+
+workflows:
+ version: 2
+ jobs:
+ - job1
+ - job2
+ - job3:
+ requires:
+ - job1
+ - job2
+ - job4:
+ requires:
+ - job3
+```
+
+Example of the same workflow as `stages` in GitLab CI/CD:
+
+```yaml
+stages:
+ - build
+ - test
+ - deploy
+
+job 1:
+ stage: build
+ script: make build dependencies
+
+job 2:
+ stage: build
+ script: make build artifacts
+
+job3:
+ stage: test
+ script: make test
+
+job4:
+ stage: deploy
+ script: make deploy
+```
+
+#### Scheduled run
+
+GitLab CI/CD has an easy to use UI to [schedule pipelines](../pipelines/schedules.md). Also, [rules](../yaml/README.md#rules) can be used to determine if jobs should be included or excluded from a scheduled pipeline.
+
+CircleCI example of a scheduled workflow:
+
+```yaml
+commit-workflow:
+ jobs:
+ - build
+scheduled-workflow:
+ triggers:
+ - schedule:
+ cron: "0 1 * * *"
+ filters:
+ branches:
+ only: try-schedule-workflow
+ jobs:
+ - build
+```
+
+Example of the same scheduled pipeline using [`rules`](../yaml/README.md#rules) in GitLab CI/CD:
+
+```yaml
+job1:
+ script:
+ - make build
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_REF_NAME == "try-schedule-workflow"'
+```
+
+After the pipeline configuration is saved, you configure the cron schedule in the [GitLab UI](../pipelines/schedules.md#configuring-pipeline-schedules), and can enable or disable schedules in the UI as well.
+
+#### Manual run
+
+CircleCI example of a manual workflow:
+
+```yaml
+release-branch-workflow:
+ jobs:
+ - build
+ - testing:
+ requires:
+ - build
+ - deploy:
+ type: approval
+ requires:
+ - testing
+```
+
+Example of the same workflow using [`when: manual`](../yaml/README.md#whenmanual) in GitLab CI/CD:
+
+```yaml
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ when: manual
+```
+
+### Filter job by branch
+
+[Rules](../yaml/README.md#rules) are a mechanism to determine if the job will or will not run for a specific branch.
+
+CircleCI example of a job filtered by branch:
+
+```yaml
+jobs:
+ deploy:
+ branches:
+ only:
+ - master
+ - /rc-.*/
+```
+
+Example of the same workflow using `rules` in GitLab CI/CD:
+
+```yaml
+deploy_prod:
+ stage: deploy
+ script:
+ - echo "Deploy to production server"
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "master"'
+```
+
+### Caching
+
+GitLab provides a caching mechanism to speed up build times for your jobs by reusing previously downloaded dependencies. It's important to know the different between [cache and artifacts](../caching/index.md#cache-vs-artifacts) to make the best use of these features.
+
+CircleCI example of a job using a cache:
+
+```yaml
+jobs:
+ job1:
+ steps:
+ - restore_cache:
+ key: source-v1-< .Revision >
+ - checkout
+ - run: npm install
+ - save_cache:
+ key: source-v1-< .Revision >
+ paths:
+ - "node_modules"
+```
+
+Example of the same pipeline using `cache` in GitLab CI/CD:
+
+```yaml
+image: node:latest
+
+# Cache modules in between jobs
+cache:
+ key: $CI_COMMIT_REF_SLUG
+ paths:
+ - .npm/
+
+before_script:
+ - npm ci --cache .npm --prefer-offline
+
+test_async:
+ script:
+ - node ./specs/start.js ./specs/async.spec.js
+```
+
+## Contexts and variables
+
+CircleCI provides [Contexts](https://circleci.com/docs/2.0/contexts/) to securely pass environment variables across project pipelines. In GitLab, a [Group](../../user/group/index.md) can be created to assemble related projects together. At the group level, [variables](../variables/README.md#group-level-environment-variables) can be stored outside the individual projects, and securely passed into pipelines across multiple projects.
+
+## Orbs
+
+There are two GitLab issues open addressing CircleCI Orbs and how GitLab can achieve similar functionality.
+
+- <https://gitlab.com/gitlab-com/Product/-/issues/1151>
+- <https://gitlab.com/gitlab-org/gitlab/-/issues/195173>
+
+## Build environments
+
+CircleCI offers `executors` as the underlying technology to run a specific job. In GitLab, this is done by [Runners](https://docs.gitlab.com/runner/).
+
+The following environments are supported:
+
+Self-Managed Runners:
+
+- Linux
+- Windows
+- macOS
+
+GitLab.com Shared Runners:
+
+- Linux
+- Windows
+- [Planned: macOS](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/5720)
+
+### Machine and specific build environments
+
+[Tags](../yaml/README.md#tags) can be used to run jobs on different platforms, by telling GitLab which Runners should run the jobs.
+
+CircleCI example of a job running on a specific environment:
+
+```yaml
+jobs:
+ ubuntuJob:
+ machine:
+ image: ubuntu-1604:201903-01
+ steps:
+ - checkout
+ - run: echo "Hello, $USER!"
+ osxJob:
+ macos:
+ xcode: 11.3.0
+ steps:
+ - checkout
+ - run: echo "Hello, $USER!"
+```
+
+Example of the same job using `tags` in GitLab CI/CD:
+
+```yaml
+windows job:
+ stage:
+ - build
+ tags:
+ - windows
+ script:
+ - echo Hello, %USERNAME%!
+
+osx job:
+ stage:
+ - build
+ tags:
+ - osx
+ script:
+ - echo "Hello, $USER!"
+```
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index 9ebcf258ee9..8dc019d3fe1 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -22,6 +22,11 @@ This guide will help you get started with Git through the command line and can b
for Git commands in the future. If you're only looking for a quick reference of Git commands, you
can download GitLab's [Git Cheat Sheet](https://about.gitlab.com/images/press/git-cheat-sheet.pdf).
+> For more information about the advantages of working with Git and GitLab:
+>
+> - Watch the [GitLab Source Code Management Walkthrough](https://www.youtube.com/watch?v=wTQ3aXJswtM) video.
+> - Learn how GitLab became the backbone of [Worldline](https://about.gitlab.com/customers/worldline/)’s development environment.
+
TIP: **Tip:**
To help you visualize what you're doing locally, there are
[Git GUI apps](https://git-scm.com/download/gui/) you can install.
diff --git a/doc/topics/git/numerous_undo_possibilities_in_git/index.md b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
index 8597325db7b..2b67edc25a3 100644
--- a/doc/topics/git/numerous_undo_possibilities_in_git/index.md
+++ b/doc/topics/git/numerous_undo_possibilities_in_git/index.md
@@ -26,6 +26,11 @@ This means that until Git automatically cleans detached commits (which cannot be
accessed by branch or tag) it will be possible to view them with `git reflog` command
and access them with direct commit ID. Read more about _[redoing the undo](#redoing-the-undo)_ in the section below.
+> For more information about working with Git and GitLab:
+>
+> - Learn why [North Western Mutual chose GitLab](https://youtu.be/kPNMyxKRRoM) for their Enterprise source code management.
+> - Learn how to [get started with Git](https://about.gitlab.com/resources/whitepaper-moving-to-git/).
+
## Introduction
This guide is organized depending on the [stage of development](https://git-scm.com/book/en/v2/Git-Basics-Recording-Changes-to-the-Repository)
diff --git a/lib/gitlab/metrics/elasticsearch_rack_middleware.rb b/lib/gitlab/metrics/elasticsearch_rack_middleware.rb
new file mode 100644
index 00000000000..6830eed68d5
--- /dev/null
+++ b/lib/gitlab/metrics/elasticsearch_rack_middleware.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ # Rack middleware for tracking Elasticsearch metrics from Grape and Web requests.
+ class ElasticsearchRackMiddleware
+ HISTOGRAM_BUCKETS = [0.1, 0.25, 0.5, 1, 2.5, 5, 10, 60].freeze
+
+ def initialize(app)
+ @app = app
+
+ @requests_total_counter = Gitlab::Metrics.counter(:http_elasticsearch_requests_total,
+ 'Amount of calls to Elasticsearch servers during web requests',
+ Gitlab::Metrics::Transaction::BASE_LABELS)
+ @requests_duration_histogram = Gitlab::Metrics.histogram(:http_elasticsearch_requests_duration_seconds,
+ 'Query time for Elasticsearch servers during web requests',
+ Gitlab::Metrics::Transaction::BASE_LABELS,
+ HISTOGRAM_BUCKETS)
+ end
+
+ def call(env)
+ transaction = Gitlab::Metrics.current_transaction
+
+ @app.call(env)
+ ensure
+ record_metrics(transaction)
+ end
+
+ private
+
+ def record_metrics(transaction)
+ labels = transaction.labels
+ query_time = ::Gitlab::Instrumentation::ElasticsearchTransport.query_time
+ request_count = ::Gitlab::Instrumentation::ElasticsearchTransport.get_request_count
+
+ @requests_total_counter.increment(labels, request_count)
+ @requests_duration_histogram.observe(labels, query_time)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/redis_rack_middleware.rb b/lib/gitlab/metrics/redis_rack_middleware.rb
index 0ed5e786fa3..f0f99c5f45d 100644
--- a/lib/gitlab/metrics/redis_rack_middleware.rb
+++ b/lib/gitlab/metrics/redis_rack_middleware.rb
@@ -6,6 +6,14 @@ module Gitlab
class RedisRackMiddleware
def initialize(app)
@app = app
+
+ @requests_total_counter = Gitlab::Metrics.counter(:http_redis_requests_total,
+ 'Amount of calls to Redis servers during web requests',
+ Gitlab::Metrics::Transaction::BASE_LABELS)
+ @requests_duration_histogram = Gitlab::Metrics.histogram(:http_redis_requests_duration_seconds,
+ 'Query time for Redis servers during web requests',
+ Gitlab::Metrics::Transaction::BASE_LABELS,
+ Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
end
def call(env)
@@ -13,7 +21,7 @@ module Gitlab
@app.call(env)
ensure
- record_metrics(transaction) if transaction
+ record_metrics(transaction)
end
private
@@ -23,14 +31,8 @@ module Gitlab
query_time = Gitlab::Instrumentation::Redis.query_time
request_count = Gitlab::Instrumentation::Redis.get_request_count
- Gitlab::Metrics.counter(:http_redis_requests_total,
- 'Amount of calls to Redis servers during web requests',
- Gitlab::Metrics::Transaction::BASE_LABELS).increment(labels, request_count)
-
- Gitlab::Metrics.histogram(:http_redis_requests_duration_seconds,
- 'Query time for Redis servers during web requests',
- Gitlab::Metrics::Transaction::BASE_LABELS,
- Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS).observe(labels, query_time)
+ @requests_total_counter.increment(labels, request_count)
+ @requests_duration_histogram.observe(labels, query_time)
end
end
end
diff --git a/lib/gitlab/setup_helper.rb b/lib/gitlab/setup_helper.rb
index 7e41adc67bd..64a30fbe16c 100644
--- a/lib/gitlab/setup_helper.rb
+++ b/lib/gitlab/setup_helper.rb
@@ -79,6 +79,7 @@ module Gitlab
config[:'gitaly-ruby'] = { dir: File.join(gitaly_dir, 'ruby') } if gitaly_ruby
config[:'gitlab-shell'] = { dir: Gitlab.config.gitlab_shell.path }
config[:bin_dir] = Gitlab.config.gitaly.client_path
+ config[:gitlab] = { url: Gitlab.config.gitlab.url }
TomlRB.dump(config)
end
@@ -97,7 +98,8 @@ module Gitlab
def configuration_toml(gitaly_dir, storage_paths)
nodes = [{ storage: 'default', address: "unix:#{gitaly_dir}/gitaly.socket", primary: true, token: 'secret' }]
storages = [{ name: 'default', node: nodes }]
- config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages }
+ failover = { enabled: false }
+ config = { socket_path: "#{gitaly_dir}/praefect.socket", memory_queue_enabled: true, virtual_storage: storages, failover: failover }
config[:token] = 'secret' if Rails.env.test?
TomlRB.dump(config)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b93d56bd8b2..60e6b5bcc53 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -983,9 +983,6 @@ msgstr ""
msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
msgstr ""
-msgid "API Help"
-msgstr ""
-
msgid "API Token"
msgstr ""
@@ -1441,9 +1438,6 @@ msgstr ""
msgid "Admin Overview"
msgstr ""
-msgid "Admin Section"
-msgstr ""
-
msgid "Admin mode already enabled"
msgstr ""
@@ -7487,9 +7481,6 @@ msgstr ""
msgid "DesignManagement|%{filename} did not change."
msgstr ""
-msgid "DesignManagement|Add designs"
-msgstr ""
-
msgid "DesignManagement|Adding a design with the same filename replaces the file in a new version."
msgstr ""
@@ -7574,6 +7565,9 @@ msgstr ""
msgid "DesignManagement|To enable design management, you'll need to %{requirements_link_start}meet the requirements%{requirements_link_end}. If you need help, reach out to our %{support_link_start}support team%{support_link_end} for assistance."
msgstr ""
+msgid "DesignManagement|Upload designs"
+msgstr ""
+
msgid "DesignManagement|Upload skipped."
msgstr ""
@@ -13336,9 +13330,6 @@ msgstr ""
msgid "Markdown"
msgstr ""
-msgid "Markdown Help"
-msgstr ""
-
msgid "Markdown enabled"
msgstr ""
@@ -15654,9 +15645,6 @@ msgstr ""
msgid "Permissions"
msgstr ""
-msgid "Permissions Help"
-msgstr ""
-
msgid "Permissions, LFS, 2FA"
msgstr ""
@@ -17685,9 +17673,6 @@ msgstr ""
msgid "Public - The project can be accessed without any authentication."
msgstr ""
-msgid "Public Access Help"
-msgstr ""
-
msgid "Public deploy keys (%{deploy_keys_count})"
msgstr ""
@@ -17817,9 +17802,6 @@ msgstr ""
msgid "README"
msgstr ""
-msgid "Rake Tasks Help"
-msgstr ""
-
msgid "Raw blob request rate limit per minute"
msgstr ""
@@ -18861,9 +18843,6 @@ msgstr ""
msgid "SSH Keys"
msgstr ""
-msgid "SSH Keys Help"
-msgstr ""
-
msgid "SSH host key fingerprints"
msgstr ""
@@ -21343,9 +21322,6 @@ msgstr ""
msgid "System Hooks"
msgstr ""
-msgid "System Hooks Help"
-msgstr ""
-
msgid "System Info"
msgstr ""
@@ -23975,9 +23951,6 @@ msgstr ""
msgid "User restrictions"
msgstr ""
-msgid "User settings"
-msgstr ""
-
msgid "User was successfully created."
msgstr ""
@@ -24789,9 +24762,6 @@ msgstr ""
msgid "Webhooks"
msgstr ""
-msgid "Webhooks Help"
-msgstr ""
-
msgid "Webhooks allow you to trigger a URL if, for example, new code is pushed or a new issue is created. You can configure webhooks to listen for specific events like pushes, issues or merge requests. Group webhooks will apply to all projects in a group, allowing you to standardize webhook functionality across your entire group."
msgstr ""
@@ -25058,9 +25028,6 @@ msgstr ""
msgid "Work in progress Limit"
msgstr ""
-msgid "Workflow Help"
-msgstr ""
-
msgid "Write"
msgstr ""
diff --git a/rubocop/cop/rspec/empty_line_after_shared_example.rb b/rubocop/cop/rspec/empty_line_after_shared_example.rb
deleted file mode 100644
index 5d09565bd5a..00000000000
--- a/rubocop/cop/rspec/empty_line_after_shared_example.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-# frozen_string_literal: true
-
-require 'rubocop/rspec/final_end_location'
-require 'rubocop/rspec/blank_line_separation'
-require 'rubocop/rspec/language'
-
-module RuboCop
- module Cop
- module RSpec
- # Checks if there is an empty line after shared example blocks.
- #
- # @example
- # # bad
- # RSpec.describe Foo do
- # it_behaves_like 'do this first'
- # it_behaves_like 'does this' do
- # end
- # it_behaves_like 'does that' do
- # end
- # it_behaves_like 'do some more'
- # end
- #
- # # good
- # RSpec.describe Foo do
- # it_behaves_like 'do this first'
- # it_behaves_like 'does this' do
- # end
- #
- # it_behaves_like 'does that' do
- # end
- #
- # it_behaves_like 'do some more'
- # end
- #
- # # fair - it's ok to have non-separated without blocks
- # RSpec.describe Foo do
- # it_behaves_like 'do this first'
- # it_behaves_like 'does this'
- # end
- #
- class EmptyLineAfterSharedExample < RuboCop::Cop::Cop
- include RuboCop::RSpec::BlankLineSeparation
- include RuboCop::RSpec::Language
-
- MSG = 'Add an empty line after `%<example>s` block.'
-
- def_node_matcher :shared_examples,
- (SharedGroups::ALL + Includes::ALL).block_pattern
-
- def on_block(node)
- shared_examples(node) do
- break if last_child?(node)
-
- missing_separating_line(node) do |location|
- add_offense(node,
- location: location,
- message: format(MSG, example: node.method_name))
- end
- end
- end
- end
- end
- end
-end
diff --git a/scripts/gitaly-test-build b/scripts/gitaly-test-build
index fcf0049162b..5254d957afd 100755
--- a/scripts/gitaly-test-build
+++ b/scripts/gitaly-test-build
@@ -14,6 +14,7 @@ class GitalyTestBuild
def run
abort 'gitaly build failed' unless system(env, 'make', chdir: tmp_tests_gitaly_dir)
+ ensure_gitlab_shell_secret!
check_gitaly_config!
# Starting gitaly further validates its configuration
diff --git a/scripts/gitaly_test.rb b/scripts/gitaly_test.rb
index e6f2c9885d9..c69c4ea747b 100644
--- a/scripts/gitaly_test.rb
+++ b/scripts/gitaly_test.rb
@@ -4,6 +4,7 @@
# Please be careful when modifying this file. Your changes must work
# both for local development rspec runs, and in CI.
+require 'securerandom'
require 'socket'
module GitalyTest
@@ -11,10 +12,22 @@ module GitalyTest
File.expand_path('../tmp/tests/gitaly', __dir__)
end
+ def tmp_tests_gitlab_shell_dir
+ File.expand_path('../tmp/tests/gitlab-shell', __dir__)
+ end
+
+ def rails_gitlab_shell_secret
+ File.expand_path('../.gitlab_shell_secret', __dir__)
+ end
+
def gemfile
File.join(tmp_tests_gitaly_dir, 'ruby', 'Gemfile')
end
+ def gitlab_shell_secret_file
+ File.join(tmp_tests_gitlab_shell_dir, '.gitlab_shell_secret')
+ end
+
def env
env_hash = {
'HOME' => File.expand_path('tmp/tests'),
@@ -70,6 +83,20 @@ module GitalyTest
pid
end
+ # Taken from Gitlab::Shell.generate_and_link_secret_token
+ def ensure_gitlab_shell_secret!
+ secret_file = rails_gitlab_shell_secret
+ shell_link = gitlab_shell_secret_file
+
+ unless File.size?(secret_file)
+ File.write(secret_file, SecureRandom.hex(16))
+ end
+
+ unless File.exist?(shell_link)
+ FileUtils.ln_s(secret_file, shell_link)
+ end
+ end
+
def check_gitaly_config!
puts "Checking gitaly-ruby Gemfile..."
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 79ffa297da3..19eeabf99ee 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -211,9 +211,4 @@ describe SearchController do
end.to raise_error(ActionController::ParameterMissing)
end
end
-
- describe 'GET #autocomplete' do
- it_behaves_like 'when the user cannot read cross project', :autocomplete, { term: 'hello' }
- it_behaves_like 'with external authorization service enabled', :autocomplete, { term: 'hello' }
- end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index 1a8af335244..8ab2c7ffd64 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -13,4 +13,6 @@ require 'active_support/all'
ActiveSupport::Dependencies.autoload_paths << 'lib'
ActiveSupport::Dependencies.autoload_paths << 'ee/lib'
+ActiveSupport::Dependencies.autoload_paths << 'tooling/lib'
+
ActiveSupport::XmlMini.backend = 'Nokogiri'
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
index 185bf4a48f7..27c0ba589e6 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Design management upload button component renders inverted upload desig
variant="success"
>
- Add designs
+ Upload designs
<!---->
</gl-deprecated-button-stub>
@@ -34,7 +34,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
variant="success"
>
- Add designs
+ Upload designs
<gl-loading-icon-stub
class="ml-1"
@@ -63,7 +63,7 @@ exports[`Design management upload button component renders upload design button
variant="success"
>
- Add designs
+ Upload designs
<!---->
</gl-deprecated-button-stub>
diff --git a/spec/frontend/fixtures/static/search_autocomplete.html b/spec/frontend/fixtures/static/global_search_input.html
index 29db9020424..29db9020424 100644
--- a/spec/frontend/fixtures/static/search_autocomplete.html
+++ b/spec/frontend/fixtures/static/global_search_input.html
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 6a06b012c6c..b209ed869bf 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -8,99 +8,6 @@ describe SearchHelper do
str
end
- describe 'search_autocomplete_opts' do
- context "with no current user" do
- before do
- allow(self).to receive(:current_user).and_return(nil)
- end
-
- it "returns nil" do
- expect(search_autocomplete_opts("q")).to be_nil
- end
- end
-
- context "with a standard user" do
- let(:user) { create(:user) }
-
- before do
- allow(self).to receive(:current_user).and_return(user)
- end
-
- it "includes Help sections" do
- expect(search_autocomplete_opts("hel").size).to eq(9)
- end
-
- it "includes default sections" do
- expect(search_autocomplete_opts("dash").size).to eq(1)
- end
-
- it "does not include admin sections" do
- expect(search_autocomplete_opts("admin").size).to eq(0)
- end
-
- it "does not allow regular expression in search term" do
- expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
- end
-
- it "includes the user's groups" do
- create(:group).add_owner(user)
- expect(search_autocomplete_opts("gro").size).to eq(1)
- end
-
- it "includes nested group" do
- create(:group, :nested, name: 'foo').add_owner(user)
- expect(search_autocomplete_opts('foo').size).to eq(1)
- end
-
- it "includes the user's projects" do
- project = create(:project, namespace: create(:namespace, owner: user))
- expect(search_autocomplete_opts(project.name).size).to eq(1)
- end
-
- it "includes the required project attrs" do
- project = create(:project, namespace: create(:namespace, owner: user))
- result = search_autocomplete_opts(project.name).first
-
- expect(result.keys).to match_array(%i[category id value label url avatar_url])
- end
-
- it "includes the required group attrs" do
- create(:group).add_owner(user)
- result = search_autocomplete_opts("gro").first
-
- expect(result.keys).to match_array(%i[category id label url avatar_url])
- end
-
- it "does not include the public group" do
- group = create(:group)
- expect(search_autocomplete_opts(group.name).size).to eq(0)
- end
-
- context "with a current project" do
- before do
- @project = create(:project, :repository)
- end
-
- it "includes project-specific sections" do
- expect(search_autocomplete_opts("Files").size).to eq(1)
- expect(search_autocomplete_opts("Commits").size).to eq(1)
- end
- end
- end
-
- context 'with an admin user' do
- let(:admin) { create(:admin) }
-
- before do
- allow(self).to receive(:current_user).and_return(admin)
- end
-
- it "includes admin sections" do
- expect(search_autocomplete_opts("admin").size).to eq(1)
- end
- end
- end
-
describe 'search_entries_info' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/global_search_input_spec.js
index 4f42d4880e8..00ae8a8f2ea 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/global_search_input_spec.js
@@ -2,10 +2,10 @@
import $ from 'jquery';
import '~/gl_dropdown';
-import initSearchAutocomplete from '~/search_autocomplete';
+import initGlobalSearchInput from '~/global_search_input';
import '~/lib/utils/common_utils';
-describe('Search autocomplete dropdown', () => {
+describe('Global search input dropdown', () => {
let widget = null;
const userName = 'root';
@@ -112,15 +112,15 @@ describe('Search autocomplete dropdown', () => {
expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
};
- preloadFixtures('static/search_autocomplete.html');
+ preloadFixtures('static/global_search_input.html');
beforeEach(function() {
- loadFixtures('static/search_autocomplete.html');
+ loadFixtures('static/global_search_input.html');
window.gon = {};
window.gon.current_user_id = userId;
window.gon.current_username = userName;
- return (widget = initSearchAutocomplete());
+ return (widget = initGlobalSearchInput());
});
afterEach(function() {
@@ -189,25 +189,25 @@ describe('Search autocomplete dropdown', () => {
expect(submitSpy).not.toHaveBeenTriggered();
});
- describe('disableAutocomplete', function() {
+ describe('disableDropdown', function() {
beforeEach(function() {
- widget.enableAutocomplete();
+ widget.enableDropdown();
});
it('should close the Dropdown', function() {
const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
widget.dropdown.addClass('show');
- widget.disableAutocomplete();
+ widget.disableDropdown();
expect(toggleSpy).toHaveBeenCalledWith('toggle');
});
});
- describe('enableAutocomplete', function() {
+ describe('enableDropdown', function() {
it('should open the Dropdown', function() {
const toggleSpy = spyOn(widget.dropdownToggle, 'dropdown');
- widget.enableAutocomplete();
+ widget.enableDropdown();
expect(toggleSpy).toHaveBeenCalledWith('toggle');
});
diff --git a/spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb
new file mode 100644
index 00000000000..305768ef060
--- /dev/null
+++ b/spec/lib/gitlab/metrics/elasticsearch_rack_middleware_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::ElasticsearchRackMiddleware do
+ let(:app) { double(:app, call: 'app call result') }
+ let(:middleware) { described_class.new(app) }
+ let(:env) { {} }
+ let(:transaction) { Gitlab::Metrics::WebTransaction.new(env) }
+
+ describe '#call' do
+ let(:counter) { instance_double(Prometheus::Client::Counter, increment: nil) }
+ let(:histogram) { instance_double(Prometheus::Client::Histogram, observe: nil) }
+ let(:elasticsearch_query_time) { 0.1 }
+ let(:elasticsearch_requests_count) { 2 }
+
+ before do
+ allow(Gitlab::Instrumentation::ElasticsearchTransport).to receive(:query_time) { elasticsearch_query_time }
+ allow(Gitlab::Instrumentation::ElasticsearchTransport).to receive(:get_request_count) { elasticsearch_requests_count }
+
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:http_elasticsearch_requests_total,
+ an_instance_of(String),
+ Gitlab::Metrics::Transaction::BASE_LABELS)
+ .and_return(counter)
+
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(:http_elasticsearch_requests_duration_seconds,
+ an_instance_of(String),
+ Gitlab::Metrics::Transaction::BASE_LABELS,
+ described_class::HISTOGRAM_BUCKETS)
+ .and_return(histogram)
+
+ allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction)
+ end
+
+ it 'calls the app' do
+ expect(middleware.call(env)).to eq('app call result')
+ end
+
+ it 'records elasticsearch metrics' do
+ expect(counter).to receive(:increment).with(transaction.labels, elasticsearch_requests_count)
+ expect(histogram).to receive(:observe).with(transaction.labels, elasticsearch_query_time)
+
+ middleware.call(env)
+ end
+
+ it 'records elasticsearch metrics if an error is raised' do
+ expect(counter).to receive(:increment).with(transaction.labels, elasticsearch_requests_count)
+ expect(histogram).to receive(:observe).with(transaction.labels, elasticsearch_query_time)
+
+ allow(app).to receive(:call).with(env).and_raise(StandardError)
+
+ expect { middleware.call(env) }.to raise_error(StandardError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb b/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb
index 025b40b7013..f2f36ccad20 100644
--- a/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/redis_rack_middleware_spec.rb
@@ -13,68 +13,49 @@ describe Gitlab::Metrics::RedisRackMiddleware do
end
describe '#call' do
- context 'when metrics are disabled' do
- before do
- allow(Gitlab::Metrics).to receive(:current_transaction).and_return(nil)
- end
-
- it 'calls the app' do
- expect(middleware.call(env)).to eq('wub wub')
- end
-
- it 'does not record metrics' do
- expect(Gitlab::Metrics).not_to receive(:counter)
- expect(Gitlab::Metrics).not_to receive(:histogram)
-
- middleware.call(env)
- end
+ let(:counter) { double(Prometheus::Client::Counter, increment: nil) }
+ let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) }
+ let(:redis_query_time) { 0.1 }
+ let(:redis_requests_count) { 2 }
+
+ before do
+ allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time }
+ allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count }
+
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:http_redis_requests_total,
+ an_instance_of(String),
+ Gitlab::Metrics::Transaction::BASE_LABELS)
+ .and_return(counter)
+
+ allow(Gitlab::Metrics).to receive(:histogram)
+ .with(:http_redis_requests_duration_seconds,
+ an_instance_of(String),
+ Gitlab::Metrics::Transaction::BASE_LABELS,
+ Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
+ .and_return(histogram)
+
+ allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction)
end
- context 'when metrics are enabled' do
- let(:counter) { double(Prometheus::Client::Counter, increment: nil) }
- let(:histogram) { double(Prometheus::Client::Histogram, observe: nil) }
- let(:redis_query_time) { 0.1 }
- let(:redis_requests_count) { 2 }
-
- before do
- allow(Gitlab::Instrumentation::Redis).to receive(:query_time) { redis_query_time }
- allow(Gitlab::Instrumentation::Redis).to receive(:get_request_count) { redis_requests_count }
-
- allow(Gitlab::Metrics).to receive(:counter)
- .with(:http_redis_requests_total,
- an_instance_of(String),
- Gitlab::Metrics::Transaction::BASE_LABELS)
- .and_return(counter)
-
- allow(Gitlab::Metrics).to receive(:histogram)
- .with(:http_redis_requests_duration_seconds,
- an_instance_of(String),
- Gitlab::Metrics::Transaction::BASE_LABELS,
- Gitlab::Instrumentation::Redis::QUERY_TIME_BUCKETS)
- .and_return(histogram)
-
- allow(Gitlab::Metrics).to receive(:current_transaction).and_return(transaction)
- end
-
- it 'calls the app' do
- expect(middleware.call(env)).to eq('wub wub')
- end
+ it 'calls the app' do
+ expect(middleware.call(env)).to eq('wub wub')
+ end
- it 'records redis metrics' do
- expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
- expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
+ it 'records redis metrics' do
+ expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
+ expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
- middleware.call(env)
- end
+ middleware.call(env)
+ end
- it 'records redis metrics if an error is raised' do
- expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
- expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
+ it 'records redis metrics if an error is raised' do
+ expect(counter).to receive(:increment).with(transaction.labels, redis_requests_count)
+ expect(histogram).to receive(:observe).with(transaction.labels, redis_query_time)
- allow(app).to receive(:call).with(env).and_raise(StandardError)
+ allow(app).to receive(:call).with(env).and_raise(StandardError)
- expect { middleware.call(env) }.to raise_error(StandardError)
- end
+ expect { middleware.call(env) }.to raise_error(StandardError)
end
end
end
diff --git a/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb b/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb
deleted file mode 100644
index cee593fe535..00000000000
--- a/spec/rubocop/cop/rspec/empty_line_after_shared_example_spec.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-require_relative '../../../../rubocop/cop/rspec/empty_line_after_shared_example'
-
-describe RuboCop::Cop::RSpec::EmptyLineAfterSharedExample do
- subject(:cop) { described_class.new }
-
- it 'flags a missing empty line after `it_behaves_like` block' do
- expect_offense(<<-RUBY)
- RSpec.describe Foo do
- it_behaves_like 'does this' do
- end
- ^^^ Add an empty line after `it_behaves_like` block.
- it_behaves_like 'does that' do
- end
- end
- RUBY
-
- expect_correction(<<-RUBY)
- RSpec.describe Foo do
- it_behaves_like 'does this' do
- end
-
- it_behaves_like 'does that' do
- end
- end
- RUBY
- end
-
- it 'ignores one-line shared examples before shared example blocks' do
- expect_no_offenses(<<-RUBY)
- RSpec.describe Foo do
- it_behaves_like 'does this'
- it_behaves_like 'does that' do
- end
- end
- RUBY
- end
-
- it 'flags a missing empty line after `shared_examples`' do
- expect_offense(<<-RUBY)
- RSpec.context 'foo' do
- shared_examples do
- end
- ^^^ Add an empty line after `shared_examples` block.
- shared_examples 'something gets done' do
- end
- end
- RUBY
-
- expect_correction(<<-RUBY)
- RSpec.context 'foo' do
- shared_examples do
- end
-
- shared_examples 'something gets done' do
- end
- end
- RUBY
- end
-
- it 'ignores consecutive one-liners' do
- expect_no_offenses(<<-RUBY)
- RSpec.describe Foo do
- it_behaves_like 'do this'
- it_behaves_like 'do that'
- end
- RUBY
- end
-
- it 'flags mixed one-line and multi-line shared examples' do
- expect_offense(<<-RUBY)
- RSpec.context 'foo' do
- it_behaves_like 'do this'
- it_behaves_like 'do that'
- it_behaves_like 'does this' do
- end
- ^^^ Add an empty line after `it_behaves_like` block.
- it_behaves_like 'do this'
- it_behaves_like 'do that'
- end
- RUBY
- end
-end
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
index 0a6bcb1badc..e08e1d670bf 100644
--- a/spec/services/auto_merge/base_service_spec.rb
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -82,9 +82,9 @@ describe AutoMerge::BaseService do
end
end
- context 'when failed to save' do
+ context 'when failed to save merge request' do
before do
- allow(merge_request).to receive(:save) { false }
+ allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'does not yield block' do
@@ -94,6 +94,39 @@ describe AutoMerge::BaseService do
it 'returns failed' do
is_expected.to eq(:failed)
end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(kind_of(ActiveRecord::RecordInvalid),
+ merge_request_id: merge_request.id)
+
+ subject
+ end
+ end
+
+ context 'when exception happens in yield block' do
+ def execute_with_error_in_yield
+ service.execute(merge_request) { raise 'Something went wrong' }
+ end
+
+ it 'returns failed status' do
+ expect(execute_with_error_in_yield).to eq(:failed)
+ end
+
+ it 'rollback the transaction' do
+ execute_with_error_in_yield
+
+ merge_request.reload
+ expect(merge_request).not_to be_auto_merge_enabled
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(kind_of(RuntimeError),
+ merge_request_id: merge_request.id)
+
+ execute_with_error_in_yield
+ end
end
end
@@ -162,7 +195,7 @@ describe AutoMerge::BaseService do
context 'when failed to save' do
before do
- allow(merge_request).to receive(:save) { false }
+ allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'does not yield block' do
@@ -178,9 +211,9 @@ describe AutoMerge::BaseService do
it_behaves_like 'Canceled or Dropped'
- context 'when failed to save' do
+ context 'when failed to save merge request' do
before do
- allow(merge_request).to receive(:save) { false }
+ allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'returns error status' do
@@ -188,6 +221,33 @@ describe AutoMerge::BaseService do
expect(subject[:message]).to eq("Can't cancel the automatic merge")
end
end
+
+ context 'when exception happens in yield block' do
+ def cancel_with_error_in_yield
+ service.cancel(merge_request) { raise 'Something went wrong' }
+ end
+
+ it 'returns error' do
+ result = cancel_with_error_in_yield
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Can't cancel the automatic merge")
+ end
+
+ it 'rollback the transaction' do
+ cancel_with_error_in_yield
+
+ merge_request.reload
+ expect(merge_request).to be_auto_merge_enabled
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(kind_of(RuntimeError),
+ merge_request_id: merge_request.id)
+
+ cancel_with_error_in_yield
+ end
+ end
end
describe '#abort' do
@@ -200,7 +260,7 @@ describe AutoMerge::BaseService do
context 'when failed to save' do
before do
- allow(merge_request).to receive(:save) { false }
+ allow(merge_request).to receive(:save!) { raise ActiveRecord::RecordInvalid.new }
end
it 'returns error status' do
@@ -208,5 +268,32 @@ describe AutoMerge::BaseService do
expect(subject[:message]).to eq("Can't abort the automatic merge")
end
end
+
+ context 'when exception happens in yield block' do
+ def abort_with_error_in_yield
+ service.abort(merge_request, reason) { raise 'Something went wrong' }
+ end
+
+ it 'returns error' do
+ result = abort_with_error_in_yield
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Can't abort the automatic merge")
+ end
+
+ it 'rollback the transaction' do
+ abort_with_error_in_yield
+
+ merge_request.reload
+ expect(merge_request).to be_auto_merge_enabled
+ end
+
+ it 'tracks the exception' do
+ expect(Gitlab::ErrorTracking)
+ .to receive(:track_exception).with(kind_of(RuntimeError),
+ merge_request_id: merge_request.id)
+
+ abort_with_error_in_yield
+ end
+ end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index e542f1e9108..27813bedfd6 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -339,29 +339,40 @@ describe Projects::CreateService, '#execute' do
end
end
- context 'when there is an active service template' do
- before do
- create(:prometheus_service, project: nil, template: true, active: true)
- end
+ describe 'create service for the project' do
+ subject(:project) { create_project(user, opts) }
- it 'creates a service from this template' do
- project = create_project(user, opts)
+ context 'when there is an active instance-level and an active template integration' do
+ before do
+ create(:prometheus_service, :instance, api_url: 'https://prometheus.instance.com/')
+ create(:prometheus_service, :template, api_url: 'https://prometheus.template.com/')
+ end
- expect(project.services.count).to eq 1
- expect(project.errors).to be_empty
+ it 'creates a service from the instance-level integration' do
+ expect(project.services.count).to eq(1)
+ expect(project.services.first.api_url).to eq('https://prometheus.instance.com/')
+ end
end
- end
- context 'when a bad service template is created' do
- it 'sets service to be inactive' do
- opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-foss'
- create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
+ context 'when there is an active service template' do
+ before do
+ create(:prometheus_service, :template, active: true)
+ end
- project = create_project(user, opts)
- service = project.services.first
+ it 'creates a service from the template' do
+ expect(project.services.count).to eq(1)
+ end
+ end
- expect(project).to be_persisted
- expect(service.active).to be false
+ context 'when there is an invalid integration' do
+ before do
+ create(:service, :template, type: 'DroneCiService', active: true)
+ end
+
+ it 'creates an inactive service' do
+ expect(project).to be_persisted
+ expect(project.services.first.active).to be false
+ end
end
end
diff --git a/spec/tooling/lib/tooling/test_file_finder_spec.rb b/spec/tooling/lib/tooling/test_file_finder_spec.rb
new file mode 100644
index 00000000000..7a5d39a014a
--- /dev/null
+++ b/spec/tooling/lib/tooling/test_file_finder_spec.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Tooling::TestFileFinder do
+ subject { Tooling::TestFileFinder.new(file) }
+
+ describe '#test_files' do
+ context 'when given non .rb files' do
+ let(:file) { 'app/assets/images/emoji.png' }
+
+ it 'does not return a test file' do
+ expect(subject.test_files).to be_empty
+ end
+ end
+
+ context 'when given file in app/' do
+ let(:file) { 'app/finders/admin/projects_finder.rb' }
+
+ it 'returns the matching app spec file' do
+ expect(subject.test_files).to contain_exactly('spec/finders/admin/projects_finder_spec.rb')
+ end
+ end
+
+ context 'when given file in lib/' do
+ let(:file) { 'lib/banzai/color_parser.rb' }
+
+ it 'returns the matching app spec file' do
+ expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb')
+ end
+ end
+
+ context 'when given a file in tooling/' do
+ let(:file) { 'tooling/lib/quality/test_file_finder.rb' }
+
+ it 'returns the matching tooling test' do
+ expect(subject.test_files).to contain_exactly('spec/tooling/lib/quality/test_file_finder_spec.rb')
+ end
+ end
+
+ context 'when given a test file' do
+ let(:file) { 'spec/lib/banzai/color_parser_spec.rb' }
+
+ it 'returns the matching test file itself' do
+ expect(subject.test_files).to contain_exactly('spec/lib/banzai/color_parser_spec.rb')
+ end
+ end
+
+ context 'when given an app file in ee/' do
+ let(:file) { 'ee/app/models/analytics/cycle_analytics/group_level.rb' }
+
+ it 'returns the matching ee/ test file' do
+ expect(subject.test_files).to contain_exactly('ee/spec/models/analytics/cycle_analytics/group_level_spec.rb')
+ end
+ end
+
+ context 'when given a module file in ee/' do
+ let(:file) { 'ee/app/models/ee/user.rb' }
+
+ it 'returns the matching ee/ module test file and the ee/ model test file' do
+ test_files = ['ee/spec/models/ee/user_spec.rb', 'spec/app/models/user_spec.rb']
+ expect(subject.test_files).to contain_exactly(*test_files)
+ end
+ end
+
+ context 'when given a lib file in ee/' do
+ let(:file) { 'ee/lib/flipper_session.rb' }
+
+ it 'returns the matching ee/ lib test file' do
+ expect(subject.test_files).to contain_exactly('ee/spec/lib/flipper_session_spec.rb')
+ end
+ end
+
+ context 'when given a test file in ee/' do
+ let(:file) { 'ee/spec/models/container_registry/event_spec.rb' }
+
+ it 'returns the test file itself' do
+ expect(subject.test_files).to contain_exactly('ee/spec/models/container_registry/event_spec.rb')
+ end
+ end
+
+ context 'when given a module test file in ee/' do
+ let(:file) { 'ee/spec/models/ee/appearance_spec.rb' }
+
+ it 'returns the matching module test file itself and the corresponding spec model test file' do
+ test_files = ['ee/spec/models/ee/appearance_spec.rb', 'spec/models/appearance_spec.rb']
+ expect(subject.test_files).to contain_exactly(*test_files)
+ end
+ end
+
+ context 'with foss_test_only: true' do
+ subject { Tooling::TestFileFinder.new(file, foss_test_only: true) }
+
+ context 'when given a module file in ee/' do
+ let(:file) { 'ee/app/models/ee/user.rb' }
+
+ it 'returns only the corresponding spec model test file in foss' do
+ expect(subject.test_files).to contain_exactly('spec/app/models/user_spec.rb')
+ end
+ end
+
+ context 'when given an app file in ee/' do
+ let(:file) { 'ee/app/models/approval.rb' }
+
+ it 'returns no test file in foss' do
+ expect(subject.test_files).to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/tooling/bin/find_foss_tests b/tooling/bin/find_foss_tests
new file mode 100755
index 00000000000..c694210ad40
--- /dev/null
+++ b/tooling/bin/find_foss_tests
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative '../../lib/gitlab/popen'
+require_relative '../lib/tooling/test_file_finder'
+
+require 'gitlab'
+
+gitlab_token = ENV.fetch('DANGER_GITLAB_API_TOKEN', '')
+
+Gitlab.configure do |config|
+ config.endpoint = 'https://gitlab.com/api/v4'
+ config.private_token = gitlab_token
+end
+
+output_file = ARGV.shift
+
+mr_project_path = ENV.fetch('CI_MERGE_REQUEST_PROJECT_PATH')
+mr_iid = ENV.fetch('CI_MERGE_REQUEST_IID')
+
+mr_changes = Gitlab.merge_request_changes(mr_project_path, mr_iid)
+changed_files = mr_changes.changes.map { |change| change['new_path'] }
+
+tests_to_run = changed_files.flat_map do |file|
+ test_files = Tooling::TestFileFinder.new(file, foss_test_only: true).test_files
+ test_files.select { |f| File.exist?(f) }
+end
+
+File.write(output_file, tests_to_run.uniq.join(' '))
diff --git a/tooling/lib/tooling/test_file_finder.rb b/tooling/lib/tooling/test_file_finder.rb
new file mode 100644
index 00000000000..d4a972f759a
--- /dev/null
+++ b/tooling/lib/tooling/test_file_finder.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+require 'ostruct'
+require 'set'
+
+module Tooling
+ class TestFileFinder
+ RUBY_EXTENSION = '.rb'
+ EE_PREFIX = 'ee/'
+
+ def initialize(file, foss_test_only: false)
+ @file = file
+ @foss_test_only = foss_test_only
+ @result = Set.new
+ end
+
+ def test_files
+ contexts = [ee_context, foss_context]
+ contexts.flat_map do |context|
+ match_test_files_for(context)
+ end
+
+ result.to_a
+ end
+
+ private
+
+ attr_reader :file, :foss_test_only, :result
+
+ def ee_context
+ OpenStruct.new.tap do |ee|
+ ee.app = %r{^#{EE_PREFIX}app/(.+)\.rb$} unless foss_test_only
+ ee.lib = %r{^#{EE_PREFIX}lib/(.+)\.rb$} unless foss_test_only
+ ee.spec = %r{^#{EE_PREFIX}spec/(.+)_spec.rb$} unless foss_test_only
+ ee.spec_dir = "#{EE_PREFIX}spec" unless foss_test_only
+ ee.ee_modules = %r{^#{EE_PREFIX}(?!spec)(.*\/)ee/(.+)\.rb$}
+ ee.ee_module_spec = %r{^#{EE_PREFIX}spec/(.*\/)ee/(.+)\.rb$}
+ ee.foss_spec_dir = 'spec'
+ end
+ end
+
+ def foss_context
+ OpenStruct.new.tap do |foss|
+ foss.app = %r{^app/(.+)\.rb$}
+ foss.lib = %r{^lib/(.+)\.rb$}
+ foss.tooling = %r{^(tooling/lib/.+)\.rb$}
+ foss.spec = %r{^spec/(.+)_spec.rb$}
+ foss.spec_dir = 'spec'
+ end
+ end
+
+ def match_test_files_for(context)
+ if (match = context.app&.match(file))
+ result << "#{context.spec_dir}/#{match[1]}_spec.rb"
+ end
+
+ if (match = context.lib&.match(file))
+ result << "#{context.spec_dir}/lib/#{match[1]}_spec.rb"
+ end
+
+ if (match = context.tooling&.match(file))
+ result << "#{context.spec_dir}/#{match[1]}_spec.rb"
+ end
+
+ if context.spec&.match(file)
+ result << file
+ end
+
+ if (match = context.ee_modules&.match(file))
+ result << "#{context.foss_spec_dir}/#{match[1]}#{match[2]}_spec.rb"
+ end
+
+ if (match = context.ee_module_spec&.match(file))
+ result << "#{context.foss_spec_dir}/#{match[1]}#{match[2]}.rb"
+ end
+ end
+ end
+end