Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-30 12:09:48 +00:00
parent 4ac9f1b8ea
commit 96ee4961ce
52 changed files with 405 additions and 165 deletions

View File

@ -132,6 +132,7 @@ rspec frontend_fixture:
extends: extends:
- .frontend-fixtures-base - .frontend-fixtures-base
- .frontend:rules:default-frontend-jobs - .frontend:rules:default-frontend-jobs
parallel: 2
rspec frontend_fixture as-if-foss: rspec frontend_fixture as-if-foss:
extends: extends:

View File

@ -26,6 +26,7 @@ update-tests-metadata:
- .test-metadata:rules:update-tests-metadata - .test-metadata:rules:update-tests-metadata
stage: post-test stage: post-test
dependencies: dependencies:
- retrieve-tests-metadata
- setup-test-env - setup-test-env
- rspec migration pg12 - rspec migration pg12
- rspec frontend_fixture - rspec frontend_fixture

View File

@ -1 +1 @@
d950585cb9763c6014ae2c9b7c4f4923d90c9f81 40fae4205d3ad62ca9341620146486bee8d31b28

View File

@ -1,11 +1,5 @@
<script> <script>
// We can't use v-safe-html here as the popover's title or content might contains SVGs that would import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
// be stripped by the directive's sanitizer. Instead, we fallback on v-html and we use GitLab's
// dompurify config that lets SVGs be rendered properly.
// Context: https://gitlab.com/gitlab-org/gitlab/-/issues/247207
/* eslint-disable vue/no-v-html */
import { GlPopover } from '@gitlab/ui';
import { sanitize } from '~/lib/dompurify';
const newPopover = (element) => { const newPopover = (element) => {
const { content, html, placement, title, triggers = 'focus' } = element.dataset; const { content, html, placement, title, triggers = 'focus' } = element.dataset;
@ -24,6 +18,9 @@ export default {
components: { components: {
GlPopover, GlPopover,
}, },
directives: {
SafeHtml: GlSafeHtmlDirective,
},
data() { data() {
return { return {
popovers: [], popovers: [],
@ -71,9 +68,9 @@ export default {
popoverExists(element) { popoverExists(element) {
return this.popovers.some((popover) => popover.target === element); return this.popovers.some((popover) => popover.target === element);
}, },
getSafeHtml(html) {
return sanitize(html);
}, },
safeHtmlConfig: {
ADD_TAGS: ['use'], // to support icon SVGs
}, },
}; };
</script> </script>
@ -82,10 +79,10 @@ export default {
<div> <div>
<gl-popover v-for="(popover, index) in popovers" :key="index" v-bind="popover"> <gl-popover v-for="(popover, index) in popovers" :key="index" v-bind="popover">
<template #title> <template #title>
<span v-if="popover.html" v-html="getSafeHtml(popover.title)"></span> <span v-if="popover.html" v-safe-html:[$options.safeHtmlConfig]="popover.title"></span>
<span v-else>{{ popover.title }}</span> <span v-else>{{ popover.title }}</span>
</template> </template>
<span v-if="popover.html" v-html="getSafeHtml(popover.content)"></span> <span v-if="popover.html" v-safe-html:[$options.safeHtmlConfig]="popover.content"></span>
<span v-else>{{ popover.content }}</span> <span v-else>{{ popover.content }}</span>
</gl-popover> </gl-popover>
</div> </div>

View File

@ -22,11 +22,15 @@ class ErrorTracking::Error < ApplicationRecord
def self.report_error(name:, description:, actor:, platform:, timestamp:) def self.report_error(name:, description:, actor:, platform:, timestamp:)
safe_find_or_create_by( safe_find_or_create_by(
name: name, name: name,
description: description,
actor: actor, actor: actor,
platform: platform platform: platform
) do |error| ).tap do |error|
error.update!(last_seen_at: timestamp) error.update!(
# Description can contain object id, so it can't be
# used as a group criteria for similar errors.
description: description,
last_seen_at: timestamp
)
end end
end end

View File

@ -26,8 +26,10 @@ class ProtectedBranch < ApplicationRecord
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
return true if project.empty_repo? && project.default_branch_protected? return true if project.empty_repo? && project.default_branch_protected?
Rails.cache.fetch("protected_ref-#{ref_name}-#{project.cache_key}") do
self.matching(ref_name, protected_refs: protected_refs(project)).present? self.matching(ref_name, protected_refs: protected_refs(project)).present?
end end
end
def self.allow_force_push?(project, ref_name) def self.allow_force_push?(project, ref_name)
project.protected_branches.allowing_force_push.matching(ref_name).any? project.protected_branches.allowing_force_push.matching(ref_name).any?

View File

@ -18,7 +18,7 @@ module ErrorTracking
# Together with occured_at these are 2 main attributes that we need to save here. # Together with occured_at these are 2 main attributes that we need to save here.
error.events.create!( error.events.create!(
environment: event['environment'], environment: event['environment'],
description: exception['type'], description: exception['value'],
level: event['level'], level: event['level'],
occurred_at: event['timestamp'], occurred_at: event['timestamp'],
payload: event payload: event

View File

@ -84,6 +84,7 @@ module Issues
# @param object [Issue, Project] # @param object [Issue, Project]
def issue_type_allowed?(object) def issue_type_allowed?(object)
WorkItem::Type.base_types.key?(params[:issue_type]) &&
can?(current_user, :"create_#{params[:issue_type]}", object) can?(current_user, :"create_#{params[:issue_type]}", object)
end end

View File

@ -157,7 +157,9 @@ module MergeRequests
def merge_to_ref def merge_to_ref
params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) } params = { allow_conflicts: Feature.enabled?(:display_merge_conflicts_in_diff, project) }
result = MergeRequests::MergeToRefService.new(project: project, current_user: merge_request.author, params: params).execute(merge_request) result = MergeRequests::MergeToRefService
.new(project: project, current_user: merge_request.author, params: params)
.execute(merge_request, true)
result[:status] == :success result[:status] == :success
end end

View File

@ -7,4 +7,4 @@
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions .form-actions
= f.submit 'Create', class: 'btn gl-button btn-confirm', data: { qa_selector: "add_deploy_key_button" } = f.submit 'Create', class: 'btn gl-button btn-confirm', data: { qa_selector: "add_deploy_key_button" }
= link_to 'Cancel', admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel' = link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'

View File

@ -1,6 +1,6 @@
- add_to_breadcrumbs "Users", admin_users_path - add_to_breadcrumbs _('Users'), admin_users_path
- add_to_breadcrumbs @user.name, admin_user_identities_path(@user) - add_to_breadcrumbs @user.name, admin_user_identities_path(@user)
- breadcrumb_title "Edit Identity" - breadcrumb_title _('Edit Identity')
- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users") - page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
%h3.page-title %h3.page-title
= _('Edit identity for %{user_name}') % { user_name: @user.name } = _('Edit identity for %{user_name}') % { user_name: @user.name }

View File

@ -1,4 +1,4 @@
- add_to_breadcrumbs "Users", admin_users_path - add_to_breadcrumbs _('Users'), admin_users_path
- breadcrumb_title @user.name - breadcrumb_title @user.name
- page_title _("Identities"), @user.name, _("Users") - page_title _("Identities"), @user.name, _("Users")
= render 'admin/users/head' = render 'admin/users/head'

View File

@ -1,7 +1,7 @@
- add_to_breadcrumbs "Users", admin_users_path - add_to_breadcrumbs _('Users'), admin_users_path
- add_to_breadcrumbs @user.name, admin_user_identities_path(@user) - add_to_breadcrumbs @user.name, admin_user_identities_path(@user)
- breadcrumb_title "New Identity" - breadcrumb_title _('New Identity')
- page_title _("New Identity") - page_title _('New Identity')
%h3.page-title= _('New identity') %h3.page-title= _('New identity')
%hr %hr
= render 'form' = render 'form'

View File

@ -1,4 +1,4 @@
- add_to_breadcrumbs 'Users', admin_users_path - add_to_breadcrumbs _('Users'), admin_users_path
- breadcrumb_title @user.name - breadcrumb_title @user.name
- page_title _('Impersonation Tokens'), @user.name, _('Users') - page_title _('Impersonation Tokens'), @user.name, _('Users')
- type = _('impersonation token') - type = _('impersonation token')

View File

@ -27,7 +27,7 @@
%strong %strong
= project.full_name = project.full_name
.gl-alert-actions .gl-alert-actions
= link_to s_('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-confirm btn-md gl-button' = link_to _('Disable'), admin_namespace_project_runner_project_path(project.namespace, project, runner_project), method: :delete, class: 'btn gl-alert-action btn-confirm btn-md gl-button'
%table.table{ data: { testid: 'unassigned-projects' } } %table.table{ data: { testid: 'unassigned-projects' } }
%thead %thead

View File

@ -1,7 +1,7 @@
%tr %tr
%th %th
= s_('Key') = _('Key')
%th %th
= s_('Environments') = _('Environments')
%th %th
= s_('Group') = _('Group')

View File

@ -12,5 +12,5 @@
= s_('403|Please contact your GitLab administrator to get permission.') = s_('403|Please contact your GitLab administrator to get permission.')
.action-container.js-go-back{ hidden: true } .action-container.js-go-back{ hidden: true }
%button{ type: 'button', class: 'gl-button btn btn-success' } %button{ type: 'button', class: 'gl-button btn btn-success' }
= s_('Go Back') = _('Go Back')
= render "errors/footer" = render "errors/footer"

View File

@ -24,7 +24,7 @@
- if current_user - if current_user
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } } .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } }
- if current_user.admin? - if current_user.admin?
= link_to [:admin, @group], class: 'btn btn-default gl-button btn-icon gl-mt-3 gl-mr-2', title: s_('View group in admin area'), = link_to [:admin, @group], class: 'btn btn-default gl-button btn-icon gl-mt-3 gl-mr-2', title: _('View group in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin') = sprite_icon('admin')
- if @notification_setting - if @notification_setting

View File

@ -1,4 +1,4 @@
- add_to_breadcrumbs "SSH Keys", profile_keys_path - add_to_breadcrumbs _('SSH Keys'), profile_keys_path
- breadcrumb_title @key.title - breadcrumb_title @key.title
- page_title @key.title, _('SSH Keys') - page_title @key.title, _('SSH Keys')
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout

View File

@ -112,7 +112,7 @@
= f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md gl-form-input', help: s_("Profiles|Who you represent or work for") = f.text_field :organization, label: s_('Profiles|Organization'), class: 'input-md gl-form-input', help: s_("Profiles|Who you represent or work for")
= f.text_area :bio, class: 'gl-form-input', label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters") = f.text_area :bio, class: 'gl-form-input', label: s_('Profiles|Bio'), rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters")
%hr %hr
%h5= s_("Private profile") %h5= _('Private profile')
.checkbox-icon-inline-wrapper .checkbox-icon-inline-wrapper
- private_profile_label = capture do - private_profile_label = capture do
= s_("Profiles|Don't display activity-related personal information on your profiles") = s_("Profiles|Don't display activity-related personal information on your profiles")

View File

@ -1,7 +1,7 @@
.form-actions.gl-display-flex .form-actions.gl-display-flex
= button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button' = button_tag 'Commit changes', id: 'commit-changes', class: 'gl-button btn btn-confirm js-commit-button qa-commit-button'
= link_to 'Cancel', cancel_path, = link_to _('Cancel'), cancel_path,
class: 'gl-button btn btn-default gl-ml-3', data: {confirm: leave_edit_message} class: 'gl-button btn btn-default gl-ml-3', data: {confirm: leave_edit_message}
= render 'shared/projects/edit_information' = render 'shared/projects/edit_information'

View File

@ -31,7 +31,7 @@
.project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5 .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
- if current_user - if current_user
- if current_user.admin? - if current_user.admin?
= link_to [:admin, @project], class: 'btn gl-button btn-icon gl-align-self-start gl-py-2! gl-mr-3', title: s_('View project in admin area'), = link_to [:admin, @project], class: 'btn gl-button btn-icon gl-align-self-start gl-py-2! gl-mr-3', title: _('View project in admin area'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin') = sprite_icon('admin')
.gl-display-flex.gl-align-items-start.gl-mr-3 .gl-display-flex.gl-align-items-start.gl-mr-3

View File

@ -11,7 +11,7 @@
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true } = f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", autofocus: true, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
.form-group.project-path.col-sm-6 .form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-bold' do = f.label :namespace_id, class: 'label-bold' do
%span= s_("Project URL") %span= _('Project URL')
.input-group.gl-flex-nowrap .input-group.gl-flex-nowrap
- if current_user.can_select_namespace? - if current_user.can_select_namespace?
- namespace_id = namespace_id_from(params) - namespace_id = namespace_id_from(params)

View File

@ -16,6 +16,6 @@
.form-actions .form-actions
= submit_tag _("Create directory"), class: 'btn gl-button btn-confirm' = submit_tag _("Create directory"), class: 'btn gl-button btn-confirm'
= link_to "Cancel", '#', class: "btn gl-button btn-default btn-cancel", "data-dismiss" => "modal" = link_to _('Cancel'), '#', class: "btn gl-button btn-default btn-cancel", "data-dismiss" => "modal"
= render 'shared/projects/edit_information' = render 'shared/projects/edit_information'

View File

@ -13,4 +13,4 @@
.form-group.row .form-group.row
.offset-sm-2.col-sm-10 .offset-sm-2.col-sm-10
= button_tag 'Delete file', class: 'btn gl-button btn-danger btn-remove-file' = button_tag 'Delete file', class: 'btn gl-button btn-danger btn-remove-file'
= link_to "Cancel", '#', class: "btn gl-button btn-cancel", "data-dismiss" => "modal" = link_to _('Cancel'), '#', class: "btn gl-button btn-cancel", "data-dismiss" => "modal"

View File

@ -1,4 +1,4 @@
- breadcrumb_title "Repository" - breadcrumb_title _('Repository')
- page_title @blob.path, @ref - page_title @blob.path, @ref
- signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1) - signatures_path = namespace_project_signatures_path(namespace_id: @project.namespace.full_path, project_id: @project.path, id: @last_commit, limit: 1)
- content_for :prefetch_asset_tags do - content_for :prefetch_asset_tags do

View File

@ -30,5 +30,5 @@
.form-text.text-muted Existing branch name, tag, or commit SHA .form-text.text-muted Existing branch name, tag, or commit SHA
.form-actions .form-actions
= button_tag 'Create branch', class: 'gl-button btn btn-confirm' = button_tag 'Create branch', class: 'gl-button btn btn-confirm'
= link_to 'Cancel', project_branches_path(@project), class: 'gl-button btn btn-default btn-cancel' = link_to _('Cancel'), project_branches_path(@project), class: 'gl-button btn btn-default btn-cancel'
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe

View File

@ -1,5 +1,5 @@
- page_title _('Error Details') - page_title _('Error Details')
- add_to_breadcrumbs 'Errors', project_error_tracking_index_path(@project) - add_to_breadcrumbs _('Errors'), project_error_tracking_index_path(@project)
- add_page_specific_style 'page_bundles/error_tracking_details' - add_page_specific_style 'page_bundles/error_tracking_details'
#js-error_details{ data: error_details_data(@project, @issue_id) } #js-error_details{ data: error_details_data(@project, @issue_id) }

View File

@ -42,5 +42,5 @@
%hr %hr
.clearfix .clearfix
.float-right .float-right
= link_to 'Cancel', edit_project_service_path(@project, @integration), class: 'gl-button btn btn-lg' = link_to _('Cancel'), edit_project_service_path(@project, @integration), class: 'gl-button btn btn-lg'
= f.submit 'Install', class: 'gl-button btn btn-success btn-lg' = f.submit 'Install', class: 'gl-button btn btn-success btn-lg'

View File

@ -27,7 +27,7 @@
%td %td
.float-right.btn-group .float-right.btn-group
- if can?(current_user, :play_pipeline_schedule, pipeline_schedule) - if can?(current_user, :play_pipeline_schedule, pipeline_schedule)
= link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn gl-button btn-default btn-icon' do = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: _('Play'), class: 'btn gl-button btn-default btn-icon' do
= sprite_icon('play') = sprite_icon('play')
- if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule) - if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule)
= link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn gl-button btn-default' do = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn gl-button btn-default' do

View File

@ -1,4 +1,4 @@
- breadcrumb_title "Schedules" - breadcrumb_title _('Schedules')
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project) - @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule") - page_title _("New Pipeline Schedule")
- add_page_specific_style 'page_bundles/pipeline_schedules' - add_page_specific_style 'page_bundles/pipeline_schedules'

View File

@ -16,4 +16,4 @@
.error-alert .error-alert
.gl-mt-5.gl-display-flex .gl-mt-5.gl-display-flex
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3' = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mr-3'
= link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel" = link_to _('Cancel'), project_tag_path(@project, @tag.name), class: "btn gl-button btn-default btn-cancel"

View File

@ -31,4 +31,4 @@
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button' = f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button'
- else - else
= f.submit 'Create label', class: 'btn gl-button btn-confirm js-save-button qa-label-create-button' = f.submit 'Create label', class: 'btn gl-button btn-confirm js-save-button qa-label-create-button'
= link_to 'Cancel', back_path, class: 'btn gl-button btn-default btn-cancel' = link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel'

View File

@ -60,4 +60,4 @@
- if runner.contacted_at - if runner.contacted_at
= time_ago_with_tooltip runner.contacted_at = time_ago_with_tooltip runner.contacted_at
- else - else
= s_('Never') = _('Never')

View File

@ -1,4 +1,4 @@
- add_to_breadcrumbs "Wiki", wiki_path(@wiki) - add_to_breadcrumbs _('Wiki'), wiki_path(@wiki)
- breadcrumb_title s_("Wiki|Pages") - breadcrumb_title s_("Wiki|Pages")
- page_title s_("Wiki|Pages"), _("Wiki") - page_title s_("Wiki|Pages"), _("Wiki")
- sort_title = wiki_sort_title(params[:sort]) - sort_title = wiki_sort_title(params[:sort])

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class ChangeDescriptionLimitErrorTrackingEvent < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
remove_text_limit :error_tracking_error_events, :description
add_text_limit :error_tracking_error_events, :description, 1024
end
def down
remove_text_limit :error_tracking_error_events, :description
add_text_limit :error_tracking_error_events, :description, 255
end
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
class CleanupRemainingOrphanInvites < ActiveRecord::Migration[6.1]
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
TMP_INDEX_NAME = 'tmp_idx_members_with_orphaned_invites'
QUERY_CONDITION = "invite_token IS NOT NULL AND user_id IS NOT NULL"
def up
membership = define_batchable_model('members')
add_concurrent_index :members, :id, where: QUERY_CONDITION, name: TMP_INDEX_NAME
membership.where(QUERY_CONDITION).pluck(:id).each_slice(10) do |group|
membership.where(id: group).where(QUERY_CONDITION).update_all(invite_token: nil)
end
remove_concurrent_index_by_name :members, TMP_INDEX_NAME
end
def down
remove_concurrent_index_by_name :members, TMP_INDEX_NAME if index_exists_by_name?(:members, TMP_INDEX_NAME)
end
end

View File

@ -0,0 +1 @@
ab678fb5e8ddf7e6dc84f36248440e94953d7c85ee6a50f4e5c06f32c6ee66ec

View File

@ -0,0 +1 @@
5dc6a4f9ecbd705bf8361c65b29931cde94968084e8ae7945a27acdcbd6475c8

View File

@ -12929,7 +12929,7 @@ CREATE TABLE error_tracking_error_events (
payload jsonb DEFAULT '{}'::jsonb NOT NULL, payload jsonb DEFAULT '{}'::jsonb NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL,
CONSTRAINT check_92ecc3077b CHECK ((char_length(description) <= 255)), CONSTRAINT check_92ecc3077b CHECK ((char_length(description) <= 1024)),
CONSTRAINT check_c67d5b8007 CHECK ((char_length(level) <= 255)), CONSTRAINT check_c67d5b8007 CHECK ((char_length(level) <= 255)),
CONSTRAINT check_f4b52474ad CHECK ((char_length(environment) <= 255)) CONSTRAINT check_f4b52474ad CHECK ((char_length(environment) <= 255))
); );

View File

@ -4,7 +4,7 @@ group: Memory
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
--- ---
# Sidekiq MemoryKiller # Sidekiq MemoryKiller **(FREE SELF)**
The GitLab Rails application code suffers from memory leaks. For web requests The GitLab Rails application code suffers from memory leaks. For web requests
this problem is made manageable using this problem is made manageable using

View File

@ -12065,6 +12065,9 @@ msgstr ""
msgid "Edit Group Hook" msgid "Edit Group Hook"
msgstr "" msgstr ""
msgid "Edit Identity"
msgstr ""
msgid "Edit Label" msgid "Edit Label"
msgstr "" msgstr ""

View File

@ -1,8 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
function retrieve_tests_metadata() { function retrieve_tests_metadata() {
mkdir -p knapsack/ rspec_flaky/ rspec_profiling/ mkdir -p $(dirname "$KNAPSACK_RSPEC_SUITE_REPORT_PATH") $(dirname "$FLAKY_RSPEC_SUITE_REPORT_PATH") rspec_profiling/
if [[ -n "${RETRIEVE_TESTS_METADATA_FROM_PAGES}" ]]; then
if [[ ! -f "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" ]]; then
curl --location -o "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
fi
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
curl --location -o "${FLAKY_RSPEC_SUITE_REPORT_PATH}" "https://gitlab-org.gitlab.io/gitlab/${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi
else
# ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to # ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to
# always target the canonical project here, so the branch must be hardcoded # always target the canonical project here, so the branch must be hardcoded
local project_path="gitlab-org/gitlab" local project_path="gitlab-org/gitlab"
@ -19,6 +28,7 @@ function retrieve_tests_metadata() {
if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then if [[ ! -f "${FLAKY_RSPEC_SUITE_REPORT_PATH}" ]]; then
scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}" scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_job_id}" --artifact-path "${FLAKY_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${FLAKY_RSPEC_SUITE_REPORT_PATH}"
fi fi
fi
} }
function update_tests_metadata() { function update_tests_metadata() {
@ -40,8 +50,13 @@ function update_tests_metadata() {
} }
function retrieve_tests_mapping() { function retrieve_tests_mapping() {
mkdir -p crystalball/ mkdir -p $(dirname "$RSPEC_PACKED_TESTS_MAPPING_PATH")
if [[ -n "${RETRIEVE_TESTS_METADATA_FROM_PAGES}" ]]; then
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
(curl --location -o "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" "https://gitlab-org.gitlab.io/gitlab/${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
fi
else
# ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to # ${CI_DEFAULT_BRANCH} might not be master in other forks but we want to
# always target the canonical project here, so the branch must be hardcoded # always target the canonical project here, so the branch must be hardcoded
local project_path="gitlab-org/gitlab" local project_path="gitlab-org/gitlab"
@ -53,6 +68,7 @@ function retrieve_tests_mapping() {
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
(scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}" (scripts/api/download_job_artifact.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" --job-id "${test_metadata_with_mapping_job_id}" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
fi fi
fi
scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}" scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
} }

View File

@ -14,51 +14,71 @@ class StaticAnalysis
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`" "Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
].freeze ].freeze
Task = Struct.new(:command, :duration) do
def cmd
command.join(' ')
end
end
NodeAssignment = Struct.new(:index, :tasks) do
def total_duration
return 0 if tasks.empty?
tasks.sum(&:duration)
end
end
# `gettext:updated_check` and `gitlab:sidekiq:sidekiq_queues_yml:check` will fail on FOSS installations # `gettext:updated_check` and `gitlab:sidekiq:sidekiq_queues_yml:check` will fail on FOSS installations
# (e.g. gitlab-org/gitlab-foss) since they test against a single # (e.g. gitlab-org/gitlab-foss) since they test against a single
# file that is generated by an EE installation, which can # file that is generated by an EE installation, which can
# contain values that a FOSS installation won't find. To work # contain values that a FOSS installation won't find. To work
# around this we will only enable this task on EE installations. # around this we will only enable this task on EE installations.
TASKS_BY_DURATIONS_SECONDS_DESC = { TASKS_WITH_DURATIONS_SECONDS = [
%w[bin/rake lint:haml] => 800, Task.new(%w[bin/rake lint:haml], 562),
# We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used, # We need to disable the cache for this cop since it creates files under tmp/feature_flags/*.used,
# the cache would prevent these files from being created. # the cache would prevent these files from being created.
%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false] => 600, Task.new(%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false], 800),
(Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 360, (Gitlab.ee? ? Task.new(%w[bin/rake gettext:updated_check], 360) : nil),
%w[yarn run lint:eslint:all] => 312, Task.new(%w[yarn run lint:eslint:all], 312),
%w[yarn run lint:prettier] => 162, Task.new(%w[bundle exec rubocop --parallel], 60),
%w[bin/rake gettext:lint] => 65, Task.new(%w[yarn run lint:prettier], 160),
%w[bundle exec license_finder] => 61, Task.new(%w[bin/rake gettext:lint], 85),
%w[bin/rake lint:static_verification] => 45, Task.new(%w[bundle exec license_finder], 20),
%w[bundle exec rubocop --parallel] => 40, Task.new(%w[bin/rake lint:static_verification], 35),
%w[bin/rake config_lint] => 26, Task.new(%w[bin/rake config_lint], 10),
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 15, Task.new(%w[bin/rake gitlab:sidekiq:all_queues_yml:check], 15),
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 11, (Gitlab.ee? ? Task.new(%w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check], 11) : nil),
%w[yarn run internal:stylelint] => 8, Task.new(%w[yarn run internal:stylelint], 8),
%w[scripts/lint-conflicts.sh] => 1, Task.new(%w[scripts/lint-conflicts.sh], 1),
%w[yarn run block-dependencies] => 1, Task.new(%w[yarn run block-dependencies], 1),
%w[scripts/lint-rugged] => 1, Task.new(%w[scripts/lint-rugged], 1),
%w[scripts/gemfile_lock_changed.sh] => 1, Task.new(%w[scripts/gemfile_lock_changed.sh], 1),
%w[scripts/frontend/check_no_partial_karma_jest.sh] => 1 Task.new(%w[scripts/frontend/check_no_partial_karma_jest.sh], 1)
}.reject { |k| k.nil? }.sort_by { |a| -a[1] }.to_h.keys.freeze ].compact.freeze
def run_tasks! def run_tasks!(options = {})
tasks = tasks_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i) node_assignment = tasks_to_run((ENV['CI_NODE_TOTAL'] || 1).to_i)[(ENV['CI_NODE_INDEX'] || 1).to_i - 1]
if options[:dry_run]
puts "Dry-run mode!"
return
end
static_analysis = Gitlab::Popen::Runner.new static_analysis = Gitlab::Popen::Runner.new
start_time = Time.now
static_analysis.run(tasks) do |cmd, &run| static_analysis.run(node_assignment.tasks.map(&:command)) do |command, &run|
task = node_assignment.tasks.find { |task| task.command == command }
puts puts
puts "$ #{cmd.join(' ')}" puts "$ #{task.cmd}"
result = run.call result = run.call
puts "==> Finished in #{result.duration} seconds" puts "==> Finished in #{result.duration} seconds (expected #{task.duration} seconds)"
puts puts
end end
puts puts
puts '===================================================' puts '==================================================='
puts "Node finished running all tasks in #{Time.now - start_time} seconds (expected #{node_assignment.total_duration})"
puts puts
puts puts
@ -107,16 +127,66 @@ class StaticAnalysis
.count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) } .count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
end end
def tasks_to_run(node_index, node_total) def tasks_to_run(node_total)
tasks = [] total_time = TASKS_WITH_DURATIONS_SECONDS.sum(&:duration).to_f
TASKS_BY_DURATIONS_SECONDS_DESC.each_with_index do |task, i| ideal_time_per_node = total_time / node_total
tasks << task if i % node_total == (node_index - 1) tasks_by_duration_desc = TASKS_WITH_DURATIONS_SECONDS.sort_by { |a| -a.duration }
nodes = Array.new(node_total) { |i| NodeAssignment.new(i + 1, []) }
puts "Total expected time: #{total_time}; ideal time per job: #{ideal_time_per_node}.\n\n"
puts "Tasks to distribute:"
tasks_by_duration_desc.each { |task| puts "* #{task.cmd} (#{task.duration}s)" }
# Distribute tasks optimally first
puts "\nAssigning tasks optimally."
distribute_tasks(tasks_by_duration_desc, nodes, ideal_time_per_node: ideal_time_per_node)
# Distribute remaining tasks, ordered by ascending duration
leftover_tasks = tasks_by_duration_desc - nodes.flat_map(&:tasks)
if leftover_tasks.any?
puts "\n\nAssigning remaining tasks: #{leftover_tasks.flat_map(&:cmd)}"
distribute_tasks(leftover_tasks, nodes.sort_by { |node| node.total_duration })
end end
tasks nodes.each do |node|
puts "\nExpected duration for node #{node.index}: #{node.total_duration} seconds"
node.tasks.each { |task| puts "* #{task.cmd} (#{task.duration}s)" }
end
nodes
end
def distribute_tasks(tasks, nodes, ideal_time_per_node: nil)
condition =
if ideal_time_per_node
->(task, node, ideal_time_per_node) { (task.duration + node.total_duration) <= ideal_time_per_node }
else
->(*) { true }
end
tasks.each do |task|
nodes.each do |node|
if condition.call(task, node, ideal_time_per_node)
assign_task_to_node(tasks, node, task)
break
end
end
end
end
def assign_task_to_node(remaining_tasks, node, task)
node.tasks << task
puts "Assigning #{task.command} (#{task.duration}s) to node ##{node.index}. Node total duration: #{node.total_duration}s."
end end
end end
if $0 == __FILE__ if $0 == __FILE__
StaticAnalysis.new.run_tasks! options = {}
if ARGV.include?('--dry-run')
options[:dry_run] = true
end
StaticAnalysis.new.run_tasks!(options)
end end

View File

@ -1,7 +1,5 @@
import axios from 'axios'; import axios from 'axios';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { once } from 'lodash';
import waitForPromises from 'helpers/wait_for_promises';
import Attachment from '~/content_editor/extensions/attachment'; import Attachment from '~/content_editor/extensions/attachment';
import Image from '~/content_editor/extensions/image'; import Image from '~/content_editor/extensions/image';
import Link from '~/content_editor/extensions/link'; import Link from '~/content_editor/extensions/link';
@ -20,7 +18,6 @@ const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="aut
describe('content_editor/extensions/attachment', () => { describe('content_editor/extensions/attachment', () => {
let tiptapEditor; let tiptapEditor;
let eq;
let doc; let doc;
let p; let p;
let image; let image;
@ -33,6 +30,24 @@ describe('content_editor/extensions/attachment', () => {
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' }); const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' }); const attachmentFile = new File(['foo'], 'test-file.zip', { type: 'application/zip' });
const expectDocumentAfterTransaction = ({ number, expectedDoc, action }) => {
return new Promise((resolve) => {
let counter = 1;
const handleTransaction = () => {
if (counter === number) {
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
tiptapEditor.off('update', handleTransaction);
resolve();
}
counter += 1;
};
tiptapEditor.on('update', handleTransaction);
action();
});
};
beforeEach(() => { beforeEach(() => {
renderMarkdown = jest.fn(); renderMarkdown = jest.fn();
@ -42,7 +57,6 @@ describe('content_editor/extensions/attachment', () => {
({ ({
builders: { doc, p, image, loading, link }, builders: { doc, p, image, loading, link },
eq,
} = createDocBuilder({ } = createDocBuilder({
tiptapEditor, tiptapEditor,
names: { names: {
@ -98,18 +112,14 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.OK, successResponse); mock.onPost().reply(httpStatus.OK, successResponse);
}); });
it('inserts an image with src set to the encoded image file and uploading true', (done) => { it('inserts an image with src set to the encoded image file and uploading true', async () => {
const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile }))); const expectedDoc = doc(p(image({ uploading: true, src: base64EncodedFile })));
tiptapEditor.on( await expectDocumentAfterTransaction({
'update', number: 1,
once(() => { expectedDoc,
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
done(); });
}),
);
tiptapEditor.commands.uploadAttachment({ file: imageFile });
}); });
it('updates the inserted image with canonicalSrc when upload is successful', async () => { it('updates the inserted image with canonicalSrc when upload is successful', async () => {
@ -124,11 +134,11 @@ describe('content_editor/extensions/attachment', () => {
), ),
); );
tiptapEditor.commands.uploadAttachment({ file: imageFile }); await expectDocumentAfterTransaction({
number: 2,
await waitForPromises(); expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); });
}); });
}); });
@ -137,14 +147,14 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR); mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
}); });
it('resets the doc to orginal state', async () => { it('resets the doc to original state', async () => {
const expectedDoc = doc(p('')); const expectedDoc = doc(p(''));
tiptapEditor.commands.uploadAttachment({ file: imageFile }); await expectDocumentAfterTransaction({
number: 2,
await waitForPromises(); expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); });
}); });
it('emits an error event that includes an error message', (done) => { it('emits an error event that includes an error message', (done) => {
@ -176,18 +186,14 @@ describe('content_editor/extensions/attachment', () => {
mock.onPost().reply(httpStatus.OK, successResponse); mock.onPost().reply(httpStatus.OK, successResponse);
}); });
it('inserts a loading mark', (done) => { it('inserts a loading mark', async () => {
const expectedDoc = doc(p(loading({ label: 'test-file' }))); const expectedDoc = doc(p(loading({ label: 'test-file' })));
tiptapEditor.on( await expectDocumentAfterTransaction({
'update', number: 1,
once(() => { expectedDoc,
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
done(); });
}),
);
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
}); });
it('updates the loading mark with a link with canonicalSrc and href attrs', async () => { it('updates the loading mark with a link with canonicalSrc and href attrs', async () => {
@ -204,11 +210,11 @@ describe('content_editor/extensions/attachment', () => {
), ),
); );
tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); await expectDocumentAfterTransaction({
number: 2,
await waitForPromises(); expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); });
}); });
}); });
@ -220,11 +226,11 @@ describe('content_editor/extensions/attachment', () => {
it('resets the doc to orginal state', async () => { it('resets the doc to orginal state', async () => {
const expectedDoc = doc(p('')); const expectedDoc = doc(p(''));
tiptapEditor.commands.uploadAttachment({ file: attachmentFile }); await expectDocumentAfterTransaction({
number: 2,
await waitForPromises(); expectedDoc,
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true); });
}); });
it('emits an error event that includes an error message', (done) => { it('emits an error event that includes an error message', (done) => {

View File

@ -54,17 +54,20 @@ describe('popovers/components/popovers.vue', () => {
expect(wrapper.findAll(GlPopover)).toHaveLength(1); expect(wrapper.findAll(GlPopover)).toHaveLength(1);
}); });
it('supports HTML content', async () => { describe('supports HTML content', () => {
const content = 'content with <b>HTML</b>'; const svgIcon = '<svg><use xlink:href="icons.svg#test"></use></svg>';
await buildWrapper(
createPopoverTarget({
content,
html: true,
}),
);
const html = wrapper.find(GlPopover).html();
expect(html).toContain(content); it.each`
description | content | render
${'renders html content correctly'} | ${'<b>HTML</b>'} | ${'<b>HTML</b>'}
${'removes any unsafe content'} | ${'<script>alert(XSS)</script>'} | ${''}
${'renders svg icons correctly'} | ${svgIcon} | ${svgIcon}
`('$description', async ({ content, render }) => {
await buildWrapper(createPopoverTarget({ content, html: true }));
const html = wrapper.find(GlPopover).html();
expect(html).toContain(render);
});
}); });
it.each` it.each`

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
require_migration! 'cleanup_remaining_orphan_invites'
RSpec.describe CleanupRemainingOrphanInvites, :migration do
def create_member(**extra_attributes)
defaults = {
access_level: 10,
source_id: 1,
source_type: "Project",
notification_level: 0,
type: 'ProjectMember'
}
table(:members).create!(defaults.merge(extra_attributes))
end
def create_user(**extra_attributes)
defaults = { projects_limit: 0 }
table(:users).create!(defaults.merge(extra_attributes))
end
describe '#up', :aggregate_failures do
it 'removes invite tokens for accepted records' do
record1 = create_member(invite_token: 'foo', user_id: nil)
record2 = create_member(invite_token: 'foo2', user_id: create_user(username: 'foo', email: 'foo@example.com').id)
record3 = create_member(invite_token: nil, user_id: create_user(username: 'bar', email: 'bar@example.com').id)
migrate!
expect(table(:members).find(record1.id).invite_token).to eq 'foo'
expect(table(:members).find(record2.id).invite_token).to eq nil
expect(table(:members).find(record3.id).invite_token).to eq nil
end
end
end

View File

@ -16,6 +16,24 @@ RSpec.describe ErrorTracking::Error, type: :model do
it { is_expected.to validate_presence_of(:actor) } it { is_expected.to validate_presence_of(:actor) }
end end
describe '.report_error' do
it 'updates existing record with a new timestamp' do
timestamp = Time.zone.now
reported_error = described_class.report_error(
name: error.name,
description: 'Lorem ipsum',
actor: error.actor,
platform: error.platform,
timestamp: timestamp
)
expect(reported_error.id).to eq(error.id)
expect(reported_error.last_seen_at).to eq(timestamp)
expect(reported_error.description).to eq('Lorem ipsum')
end
end
describe '#title' do describe '#title' do
it { expect(error.title).to eq('ActionView::MissingTemplate Missing template posts/edit') } it { expect(error.title).to eq('ActionView::MissingTemplate Missing template posts/edit') }
end end

View File

@ -162,6 +162,30 @@ RSpec.describe ProtectedBranch do
expect(described_class.protected?(project, 'staging/some-branch')).to eq(false) expect(described_class.protected?(project, 'staging/some-branch')).to eq(false)
end end
context 'with caching', :use_clean_rails_memory_store_caching do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:protected_branch) { create(:protected_branch, project: project, name: "jawn") }
before do
allow(described_class).to receive(:matching).once.and_call_original
# the original call works and warms the cache
described_class.protected?(project, 'jawn')
end
it 'correctly invalidates a cache' do
expect(described_class).to receive(:matching).once.and_call_original
create(:protected_branch, project: project, name: "bar")
# the cache is invalidated because the project has been "updated"
expect(described_class.protected?(project, 'jawn')).to eq(true)
end
it 'correctly uses the cached version' do
expect(described_class).not_to receive(:matching)
expect(described_class.protected?(project, 'jawn')).to eq(true)
end
end
end end
context 'new project' do context 'new project' do

View File

@ -34,7 +34,7 @@ RSpec.describe ErrorTracking::CollectErrorService do
expect(error.platform).to eq 'ruby' expect(error.platform).to eq 'ruby'
expect(error.last_seen_at).to eq '2021-07-08T12:59:16Z' expect(error.last_seen_at).to eq '2021-07-08T12:59:16Z'
expect(event.description).to eq 'ActionView::MissingTemplate' expect(event.description).to start_with 'Missing template posts/error2'
expect(event.occurred_at).to eq '2021-07-08T12:59:16Z' expect(event.occurred_at).to eq '2021-07-08T12:59:16Z'
expect(event.level).to eq 'error' expect(event.level).to eq 'error'
expect(event.environment).to eq 'development' expect(event.environment).to eq 'development'

View File

@ -183,8 +183,8 @@ RSpec.describe Issues::BuildService do
expect(issue).to be_incident expect(issue).to be_incident
end end
it 'cannot set invalid type' do it 'cannot set invalid issue type' do
issue = build_issue(issue_type: 'invalid type') issue = build_issue(issue_type: 'project')
expect(issue).to be_issue expect(issue).to be_issue
end end

View File

@ -132,6 +132,15 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
it_behaves_like 'mergeable merge request' it_behaves_like 'mergeable merge request'
it 'calls MergeToRefService with cache parameter' do
service = instance_double(MergeRequests::MergeToRefService)
expect(MergeRequests::MergeToRefService).to receive(:new).once { service }
expect(service).to receive(:execute).once.with(merge_request, true).and_return(success: true)
described_class.new(merge_request).execute(recheck: true)
end
context 'when concurrent calls' do context 'when concurrent calls' do
it 'waits first lock and returns "cached" result in subsequent calls' do it 'waits first lock and returns "cached" result in subsequent calls' do
threads = execute_within_threads(amount: 3) threads = execute_within_threads(amount: 3)