Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4ac9f1b8ea
commit
96ee4961ce
|
@ -132,6 +132,7 @@ rspec frontend_fixture:
|
|||
extends:
|
||||
- .frontend-fixtures-base
|
||||
- .frontend:rules:default-frontend-jobs
|
||||
parallel: 2
|
||||
|
||||
rspec frontend_fixture as-if-foss:
|
||||
extends:
|
||||
|
|
|
@ -26,6 +26,7 @@ update-tests-metadata:
|
|||
- .test-metadata:rules:update-tests-metadata
|
||||
stage: post-test
|
||||
dependencies:
|
||||
- retrieve-tests-metadata
|
||||
- setup-test-env
|
||||
- rspec migration pg12
|
||||
- rspec frontend_fixture
|
||||
|
|
|
@ -1 +1 @@
|
|||
d950585cb9763c6014ae2c9b7c4f4923d90c9f81
|
||||
40fae4205d3ad62ca9341620146486bee8d31b28
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
<script>
|
||||
// We can't use v-safe-html here as the popover's title or content might contains SVGs that would
|
||||
// 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';
|
||||
import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
|
||||
|
||||
const newPopover = (element) => {
|
||||
const { content, html, placement, title, triggers = 'focus' } = element.dataset;
|
||||
|
@ -24,6 +18,9 @@ export default {
|
|||
components: {
|
||||
GlPopover,
|
||||
},
|
||||
directives: {
|
||||
SafeHtml: GlSafeHtmlDirective,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
popovers: [],
|
||||
|
@ -71,9 +68,9 @@ export default {
|
|||
popoverExists(element) {
|
||||
return this.popovers.some((popover) => popover.target === element);
|
||||
},
|
||||
getSafeHtml(html) {
|
||||
return sanitize(html);
|
||||
},
|
||||
safeHtmlConfig: {
|
||||
ADD_TAGS: ['use'], // to support icon SVGs
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -82,10 +79,10 @@ export default {
|
|||
<div>
|
||||
<gl-popover v-for="(popover, index) in popovers" :key="index" v-bind="popover">
|
||||
<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>
|
||||
</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>
|
||||
</gl-popover>
|
||||
</div>
|
||||
|
|
|
@ -22,11 +22,15 @@ class ErrorTracking::Error < ApplicationRecord
|
|||
def self.report_error(name:, description:, actor:, platform:, timestamp:)
|
||||
safe_find_or_create_by(
|
||||
name: name,
|
||||
description: description,
|
||||
actor: actor,
|
||||
platform: platform
|
||||
) do |error|
|
||||
error.update!(last_seen_at: timestamp)
|
||||
).tap do |error|
|
||||
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
|
||||
|
||||
|
|
|
@ -26,8 +26,10 @@ class ProtectedBranch < ApplicationRecord
|
|||
def self.protected?(project, ref_name)
|
||||
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?
|
||||
end
|
||||
end
|
||||
|
||||
def self.allow_force_push?(project, ref_name)
|
||||
project.protected_branches.allowing_force_push.matching(ref_name).any?
|
||||
|
|
|
@ -18,7 +18,7 @@ module ErrorTracking
|
|||
# Together with occured_at these are 2 main attributes that we need to save here.
|
||||
error.events.create!(
|
||||
environment: event['environment'],
|
||||
description: exception['type'],
|
||||
description: exception['value'],
|
||||
level: event['level'],
|
||||
occurred_at: event['timestamp'],
|
||||
payload: event
|
||||
|
|
|
@ -84,6 +84,7 @@ module Issues
|
|||
|
||||
# @param object [Issue, Project]
|
||||
def issue_type_allowed?(object)
|
||||
WorkItem::Type.base_types.key?(params[:issue_type]) &&
|
||||
can?(current_user, :"create_#{params[:issue_type]}", object)
|
||||
end
|
||||
|
||||
|
|
|
@ -157,7 +157,9 @@ module MergeRequests
|
|||
|
||||
def merge_to_ref
|
||||
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
|
||||
end
|
||||
|
|
|
@ -7,4 +7,4 @@
|
|||
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
|
||||
.form-actions
|
||||
= 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'
|
||||
|
|
|
@ -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)
|
||||
- breadcrumb_title "Edit Identity"
|
||||
- breadcrumb_title _('Edit Identity')
|
||||
- page_title _("Edit"), @identity.provider, _("Identities"), @user.name, _("Users")
|
||||
%h3.page-title
|
||||
= _('Edit identity for %{user_name}') % { user_name: @user.name }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- add_to_breadcrumbs "Users", admin_users_path
|
||||
- add_to_breadcrumbs _('Users'), admin_users_path
|
||||
- breadcrumb_title @user.name
|
||||
- page_title _("Identities"), @user.name, _("Users")
|
||||
= render 'admin/users/head'
|
||||
|
|
|
@ -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)
|
||||
- breadcrumb_title "New Identity"
|
||||
- page_title _("New Identity")
|
||||
- breadcrumb_title _('New Identity')
|
||||
- page_title _('New Identity')
|
||||
%h3.page-title= _('New identity')
|
||||
%hr
|
||||
= render 'form'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- add_to_breadcrumbs 'Users', admin_users_path
|
||||
- add_to_breadcrumbs _('Users'), admin_users_path
|
||||
- breadcrumb_title @user.name
|
||||
- page_title _('Impersonation Tokens'), @user.name, _('Users')
|
||||
- type = _('impersonation token')
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
%strong
|
||||
= project.full_name
|
||||
.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' } }
|
||||
%thead
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
%tr
|
||||
%th
|
||||
= s_('Key')
|
||||
= _('Key')
|
||||
%th
|
||||
= s_('Environments')
|
||||
= _('Environments')
|
||||
%th
|
||||
= s_('Group')
|
||||
= _('Group')
|
||||
|
|
|
@ -12,5 +12,5 @@
|
|||
= s_('403|Please contact your GitLab administrator to get permission.')
|
||||
.action-container.js-go-back{ hidden: true }
|
||||
%button{ type: 'button', class: 'gl-button btn btn-success' }
|
||||
= s_('Go Back')
|
||||
= _('Go Back')
|
||||
= render "errors/footer"
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
- if current_user
|
||||
.gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } }
|
||||
- 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
|
||||
= sprite_icon('admin')
|
||||
- if @notification_setting
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- add_to_breadcrumbs "SSH Keys", profile_keys_path
|
||||
- add_to_breadcrumbs _('SSH Keys'), profile_keys_path
|
||||
- breadcrumb_title @key.title
|
||||
- page_title @key.title, _('SSH Keys')
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
|
|
|
@ -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_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
|
||||
%h5= s_("Private profile")
|
||||
%h5= _('Private profile')
|
||||
.checkbox-icon-inline-wrapper
|
||||
- private_profile_label = capture do
|
||||
= s_("Profiles|Don't display activity-related personal information on your profiles")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
.form-actions.gl-display-flex
|
||||
= 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}
|
||||
|
||||
= render 'shared/projects/edit_information'
|
||||
|
|
|
@ -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
|
||||
- if current_user
|
||||
- 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
|
||||
= sprite_icon('admin')
|
||||
.gl-display-flex.gl-align-items-start.gl-mr-3
|
||||
|
|
|
@ -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 }
|
||||
.form-group.project-path.col-sm-6
|
||||
= f.label :namespace_id, class: 'label-bold' do
|
||||
%span= s_("Project URL")
|
||||
%span= _('Project URL')
|
||||
.input-group.gl-flex-nowrap
|
||||
- if current_user.can_select_namespace?
|
||||
- namespace_id = namespace_id_from(params)
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
|
||||
.form-actions
|
||||
= 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'
|
||||
|
|
|
@ -13,4 +13,4 @@
|
|||
.form-group.row
|
||||
.offset-sm-2.col-sm-10
|
||||
= 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"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- breadcrumb_title "Repository"
|
||||
- breadcrumb_title _('Repository')
|
||||
- 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)
|
||||
- content_for :prefetch_asset_tags do
|
||||
|
|
|
@ -30,5 +30,5 @@
|
|||
.form-text.text-muted Existing branch name, tag, or commit SHA
|
||||
.form-actions
|
||||
= 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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- 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'
|
||||
|
||||
#js-error_details{ data: error_details_data(@project, @issue_id) }
|
||||
|
|
|
@ -42,5 +42,5 @@
|
|||
%hr
|
||||
.clearfix
|
||||
.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'
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
%td
|
||||
.float-right.btn-group
|
||||
- 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')
|
||||
- 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
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- breadcrumb_title "Schedules"
|
||||
- breadcrumb_title _('Schedules')
|
||||
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
|
||||
- page_title _("New Pipeline Schedule")
|
||||
- add_page_specific_style 'page_bundles/pipeline_schedules'
|
||||
|
|
|
@ -16,4 +16,4 @@
|
|||
.error-alert
|
||||
.gl-mt-5.gl-display-flex
|
||||
= 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"
|
||||
|
|
|
@ -31,4 +31,4 @@
|
|||
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button'
|
||||
- else
|
||||
= 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'
|
||||
|
|
|
@ -60,4 +60,4 @@
|
|||
- if runner.contacted_at
|
||||
= time_ago_with_tooltip runner.contacted_at
|
||||
- else
|
||||
= s_('Never')
|
||||
= _('Never')
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- add_to_breadcrumbs "Wiki", wiki_path(@wiki)
|
||||
- add_to_breadcrumbs _('Wiki'), wiki_path(@wiki)
|
||||
- breadcrumb_title s_("Wiki|Pages")
|
||||
- page_title s_("Wiki|Pages"), _("Wiki")
|
||||
- sort_title = wiki_sort_title(params[:sort])
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
ab678fb5e8ddf7e6dc84f36248440e94953d7c85ee6a50f4e5c06f32c6ee66ec
|
|
@ -0,0 +1 @@
|
|||
5dc6a4f9ecbd705bf8361c65b29931cde94968084e8ae7945a27acdcbd6475c8
|
|
@ -12929,7 +12929,7 @@ CREATE TABLE error_tracking_error_events (
|
|||
payload jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||
created_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_f4b52474ad CHECK ((char_length(environment) <= 255))
|
||||
);
|
||||
|
|
|
@ -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
|
||||
---
|
||||
|
||||
# Sidekiq MemoryKiller
|
||||
# Sidekiq MemoryKiller **(FREE SELF)**
|
||||
|
||||
The GitLab Rails application code suffers from memory leaks. For web requests
|
||||
this problem is made manageable using
|
||||
|
|
|
@ -12065,6 +12065,9 @@ msgstr ""
|
|||
msgid "Edit Group Hook"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit Identity"
|
||||
msgstr ""
|
||||
|
||||
msgid "Edit Label"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
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
|
||||
# always target the canonical project here, so the branch must be hardcoded
|
||||
local project_path="gitlab-org/gitlab"
|
||||
|
@ -19,6 +28,7 @@ function retrieve_tests_metadata() {
|
|||
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}"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
function update_tests_metadata() {
|
||||
|
@ -40,8 +50,13 @@ function update_tests_metadata() {
|
|||
}
|
||||
|
||||
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
|
||||
# always target the canonical project here, so the branch must be hardcoded
|
||||
local project_path="gitlab-org/gitlab"
|
||||
|
@ -53,6 +68,7 @@ function retrieve_tests_mapping() {
|
|||
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}"
|
||||
fi
|
||||
fi
|
||||
|
||||
scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
|
||||
}
|
||||
|
|
|
@ -14,51 +14,71 @@ class StaticAnalysis
|
|||
"Browserslist: caniuse-lite is outdated. Please run next command `yarn upgrade`"
|
||||
].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
|
||||
# (e.g. gitlab-org/gitlab-foss) since they test against a single
|
||||
# file that is generated by an EE installation, which can
|
||||
# contain values that a FOSS installation won't find. To work
|
||||
# around this we will only enable this task on EE installations.
|
||||
TASKS_BY_DURATIONS_SECONDS_DESC = {
|
||||
%w[bin/rake lint:haml] => 800,
|
||||
TASKS_WITH_DURATIONS_SECONDS = [
|
||||
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,
|
||||
# the cache would prevent these files from being created.
|
||||
%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false] => 600,
|
||||
(Gitlab.ee? ? %w[bin/rake gettext:updated_check] : nil) => 360,
|
||||
%w[yarn run lint:eslint:all] => 312,
|
||||
%w[yarn run lint:prettier] => 162,
|
||||
%w[bin/rake gettext:lint] => 65,
|
||||
%w[bundle exec license_finder] => 61,
|
||||
%w[bin/rake lint:static_verification] => 45,
|
||||
%w[bundle exec rubocop --parallel] => 40,
|
||||
%w[bin/rake config_lint] => 26,
|
||||
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 15,
|
||||
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 11,
|
||||
%w[yarn run internal:stylelint] => 8,
|
||||
%w[scripts/lint-conflicts.sh] => 1,
|
||||
%w[yarn run block-dependencies] => 1,
|
||||
%w[scripts/lint-rugged] => 1,
|
||||
%w[scripts/gemfile_lock_changed.sh] => 1,
|
||||
%w[scripts/frontend/check_no_partial_karma_jest.sh] => 1
|
||||
}.reject { |k| k.nil? }.sort_by { |a| -a[1] }.to_h.keys.freeze
|
||||
Task.new(%w[bundle exec rubocop --only Gitlab/MarkUsedFeatureFlags --cache false], 800),
|
||||
(Gitlab.ee? ? Task.new(%w[bin/rake gettext:updated_check], 360) : nil),
|
||||
Task.new(%w[yarn run lint:eslint:all], 312),
|
||||
Task.new(%w[bundle exec rubocop --parallel], 60),
|
||||
Task.new(%w[yarn run lint:prettier], 160),
|
||||
Task.new(%w[bin/rake gettext:lint], 85),
|
||||
Task.new(%w[bundle exec license_finder], 20),
|
||||
Task.new(%w[bin/rake lint:static_verification], 35),
|
||||
Task.new(%w[bin/rake config_lint], 10),
|
||||
Task.new(%w[bin/rake gitlab:sidekiq:all_queues_yml:check], 15),
|
||||
(Gitlab.ee? ? Task.new(%w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check], 11) : nil),
|
||||
Task.new(%w[yarn run internal:stylelint], 8),
|
||||
Task.new(%w[scripts/lint-conflicts.sh], 1),
|
||||
Task.new(%w[yarn run block-dependencies], 1),
|
||||
Task.new(%w[scripts/lint-rugged], 1),
|
||||
Task.new(%w[scripts/gemfile_lock_changed.sh], 1),
|
||||
Task.new(%w[scripts/frontend/check_no_partial_karma_jest.sh], 1)
|
||||
].compact.freeze
|
||||
|
||||
def run_tasks!
|
||||
tasks = tasks_to_run((ENV['CI_NODE_INDEX'] || 1).to_i, (ENV['CI_NODE_TOTAL'] || 1).to_i)
|
||||
def run_tasks!(options = {})
|
||||
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.run(tasks) do |cmd, &run|
|
||||
start_time = Time.now
|
||||
static_analysis.run(node_assignment.tasks.map(&:command)) do |command, &run|
|
||||
task = node_assignment.tasks.find { |task| task.command == command }
|
||||
puts
|
||||
puts "$ #{cmd.join(' ')}"
|
||||
puts "$ #{task.cmd}"
|
||||
|
||||
result = run.call
|
||||
|
||||
puts "==> Finished in #{result.duration} seconds"
|
||||
puts "==> Finished in #{result.duration} seconds (expected #{task.duration} seconds)"
|
||||
puts
|
||||
end
|
||||
|
||||
puts
|
||||
puts '==================================================='
|
||||
puts "Node finished running all tasks in #{Time.now - start_time} seconds (expected #{node_assignment.total_duration})"
|
||||
puts
|
||||
puts
|
||||
|
||||
|
@ -107,16 +127,66 @@ class StaticAnalysis
|
|||
.count { |result| !ALLOWED_WARNINGS.include?(result.stderr.strip) }
|
||||
end
|
||||
|
||||
def tasks_to_run(node_index, node_total)
|
||||
tasks = []
|
||||
TASKS_BY_DURATIONS_SECONDS_DESC.each_with_index do |task, i|
|
||||
tasks << task if i % node_total == (node_index - 1)
|
||||
def tasks_to_run(node_total)
|
||||
total_time = TASKS_WITH_DURATIONS_SECONDS.sum(&:duration).to_f
|
||||
ideal_time_per_node = total_time / node_total
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
if $0 == __FILE__
|
||||
StaticAnalysis.new.run_tasks!
|
||||
options = {}
|
||||
|
||||
if ARGV.include?('--dry-run')
|
||||
options[:dry_run] = true
|
||||
end
|
||||
|
||||
StaticAnalysis.new.run_tasks!(options)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import axios from 'axios';
|
||||
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 Image from '~/content_editor/extensions/image';
|
||||
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', () => {
|
||||
let tiptapEditor;
|
||||
let eq;
|
||||
let doc;
|
||||
let p;
|
||||
let image;
|
||||
|
@ -33,6 +30,24 @@ describe('content_editor/extensions/attachment', () => {
|
|||
const imageFile = new File(['foo'], 'test-file.png', { type: 'image/png' });
|
||||
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(() => {
|
||||
renderMarkdown = jest.fn();
|
||||
|
||||
|
@ -42,7 +57,6 @@ describe('content_editor/extensions/attachment', () => {
|
|||
|
||||
({
|
||||
builders: { doc, p, image, loading, link },
|
||||
eq,
|
||||
} = createDocBuilder({
|
||||
tiptapEditor,
|
||||
names: {
|
||||
|
@ -98,18 +112,14 @@ describe('content_editor/extensions/attachment', () => {
|
|||
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 })));
|
||||
|
||||
tiptapEditor.on(
|
||||
'update',
|
||||
once(() => {
|
||||
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
|
||||
done();
|
||||
}),
|
||||
);
|
||||
|
||||
tiptapEditor.commands.uploadAttachment({ file: imageFile });
|
||||
await expectDocumentAfterTransaction({
|
||||
number: 1,
|
||||
expectedDoc,
|
||||
action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
|
||||
});
|
||||
});
|
||||
|
||||
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 waitForPromises();
|
||||
|
||||
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
|
||||
await expectDocumentAfterTransaction({
|
||||
number: 2,
|
||||
expectedDoc,
|
||||
action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -137,14 +147,14 @@ describe('content_editor/extensions/attachment', () => {
|
|||
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(''));
|
||||
|
||||
tiptapEditor.commands.uploadAttachment({ file: imageFile });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
|
||||
await expectDocumentAfterTransaction({
|
||||
number: 2,
|
||||
expectedDoc,
|
||||
action: () => tiptapEditor.commands.uploadAttachment({ file: imageFile }),
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
it('inserts a loading mark', (done) => {
|
||||
it('inserts a loading mark', async () => {
|
||||
const expectedDoc = doc(p(loading({ label: 'test-file' })));
|
||||
|
||||
tiptapEditor.on(
|
||||
'update',
|
||||
once(() => {
|
||||
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
|
||||
done();
|
||||
}),
|
||||
);
|
||||
|
||||
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
|
||||
await expectDocumentAfterTransaction({
|
||||
number: 1,
|
||||
expectedDoc,
|
||||
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
|
||||
});
|
||||
});
|
||||
|
||||
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 waitForPromises();
|
||||
|
||||
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
|
||||
await expectDocumentAfterTransaction({
|
||||
number: 2,
|
||||
expectedDoc,
|
||||
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -220,11 +226,11 @@ describe('content_editor/extensions/attachment', () => {
|
|||
it('resets the doc to orginal state', async () => {
|
||||
const expectedDoc = doc(p(''));
|
||||
|
||||
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
|
||||
await expectDocumentAfterTransaction({
|
||||
number: 2,
|
||||
expectedDoc,
|
||||
action: () => tiptapEditor.commands.uploadAttachment({ file: attachmentFile }),
|
||||
});
|
||||
});
|
||||
|
||||
it('emits an error event that includes an error message', (done) => {
|
||||
|
|
|
@ -54,17 +54,20 @@ describe('popovers/components/popovers.vue', () => {
|
|||
expect(wrapper.findAll(GlPopover)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('supports HTML content', async () => {
|
||||
const content = 'content with <b>HTML</b>';
|
||||
await buildWrapper(
|
||||
createPopoverTarget({
|
||||
content,
|
||||
html: true,
|
||||
}),
|
||||
);
|
||||
const html = wrapper.find(GlPopover).html();
|
||||
describe('supports HTML content', () => {
|
||||
const svgIcon = '<svg><use xlink:href="icons.svg#test"></use></svg>';
|
||||
|
||||
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`
|
||||
|
|
|
@ -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
|
|
@ -16,6 +16,24 @@ RSpec.describe ErrorTracking::Error, type: :model do
|
|||
it { is_expected.to validate_presence_of(:actor) }
|
||||
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
|
||||
it { expect(error.title).to eq('ActionView::MissingTemplate Missing template posts/edit') }
|
||||
end
|
||||
|
|
|
@ -162,6 +162,30 @@ RSpec.describe ProtectedBranch do
|
|||
|
||||
expect(described_class.protected?(project, 'staging/some-branch')).to eq(false)
|
||||
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
|
||||
|
||||
context 'new project' do
|
||||
|
|
|
@ -34,7 +34,7 @@ RSpec.describe ErrorTracking::CollectErrorService do
|
|||
expect(error.platform).to eq 'ruby'
|
||||
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.level).to eq 'error'
|
||||
expect(event.environment).to eq 'development'
|
||||
|
|
|
@ -183,8 +183,8 @@ RSpec.describe Issues::BuildService do
|
|||
expect(issue).to be_incident
|
||||
end
|
||||
|
||||
it 'cannot set invalid type' do
|
||||
issue = build_issue(issue_type: 'invalid type')
|
||||
it 'cannot set invalid issue type' do
|
||||
issue = build_issue(issue_type: 'project')
|
||||
|
||||
expect(issue).to be_issue
|
||||
end
|
||||
|
|
|
@ -132,6 +132,15 @@ RSpec.describe MergeRequests::MergeabilityCheckService, :clean_gitlab_redis_shar
|
|||
|
||||
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
|
||||
it 'waits first lock and returns "cached" result in subsequent calls' do
|
||||
threads = execute_within_threads(amount: 3)
|
||||
|
|
Loading…
Reference in New Issue