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:
- .frontend-fixtures-base
- .frontend:rules:default-frontend-jobs
parallel: 2
rspec frontend_fixture as-if-foss:
extends:

View File

@ -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

View File

@ -1 +1 @@
d950585cb9763c6014ae2c9b7c4f4923d90c9f81
40fae4205d3ad62ca9341620146486bee8d31b28

View File

@ -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>

View File

@ -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

View File

@ -26,7 +26,9 @@ class ProtectedBranch < ApplicationRecord
def self.protected?(project, ref_name)
return true if project.empty_repo? && project.default_branch_protected?
self.matching(ref_name, protected_refs: protected_refs(project)).present?
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)

View File

@ -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

View File

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

View File

@ -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

View File

@ -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'

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)
- 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 }

View File

@ -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'

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)
- breadcrumb_title "New Identity"
- page_title _("New Identity")
- breadcrumb_title _('New Identity')
- page_title _('New Identity')
%h3.page-title= _('New identity')
%hr
= 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
- page_title _('Impersonation Tokens'), @user.name, _('Users')
- type = _('impersonation token')

View File

@ -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

View File

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

View File

@ -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"

View File

@ -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

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
- page_title @key.title, _('SSH Keys')
- @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_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")

View File

@ -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'

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
- 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

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 }
.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)

View File

@ -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'

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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"

View File

@ -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'

View File

@ -60,4 +60,4 @@
- if runner.contacted_at
= time_ago_with_tooltip runner.contacted_at
- 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")
- page_title s_("Wiki|Pages"), _("Wiki")
- 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,
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))
);

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
---
# Sidekiq MemoryKiller
# Sidekiq MemoryKiller **(FREE SELF)**
The GitLab Rails application code suffers from memory leaks. For web requests
this problem is made manageable using

View File

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

View File

@ -1,23 +1,33 @@
#!/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/
# ${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"
local artifact_branch="master"
local test_metadata_job_id
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
# Ruby
test_metadata_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata")
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"
local artifact_branch="master"
local test_metadata_job_id
if [[ ! -f "${KNAPSACK_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 "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
fi
# Ruby
test_metadata_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-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}"
if [[ ! -f "${KNAPSACK_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 "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" || echo "{}" > "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}"
fi
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
}
@ -40,18 +50,24 @@ function update_tests_metadata() {
}
function retrieve_tests_mapping() {
mkdir -p crystalball/
mkdir -p $(dirname "$RSPEC_PACKED_TESTS_MAPPING_PATH")
# ${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"
local artifact_branch="master"
local test_metadata_with_mapping_job_id
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"
local artifact_branch="master"
local test_metadata_with_mapping_job_id
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz")
test_metadata_with_mapping_job_id=$(scripts/api/get_job_id.rb --endpoint "https://gitlab.com/api/v4" --project "${project_path}" -q "status=success" -q "ref=${artifact_branch}" -q "username=gitlab-bot" -Q "scope=success" --job-name "update-tests-metadata" --artifact-path "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz")
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}"
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}"

View File

@ -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

View File

@ -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) => {

View File

@ -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`

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) }
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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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)