Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-11-20 15:16:34 +00:00
parent 3205d8a555
commit 3d53332cde
84 changed files with 25545 additions and 46485 deletions

View File

@ -404,9 +404,9 @@ Dangerfile
^[Product Analytics] @gitlab-org/analytics-section/product-analytics/engineers/frontend
/app/assets/javascripts/vue_shared/components/customizable_dashboard/
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_dashboard.vue
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_visualization_designer.vue
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/analytics_data_explorer.vue
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/dashboards_list.vue
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualization_designer/selectors/product_analytics/
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/data_explorer/selectors/product_analytics/
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/column_chart.vue
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/data_table.vue
/ee/app/assets/javascripts/analytics/analytics_dashboards/components/visualizations/line_chart.vue

View File

@ -16,7 +16,6 @@ RSpec/AvoidConditionalStatements:
- 'ee/spec/features/issues/user_sees_empty_state_spec.rb'
- 'ee/spec/features/labels_hierarchy_spec.rb'
- 'ee/spec/features/profiles/usage_quotas_spec.rb'
- 'ee/spec/features/projects/analytics/visualization_designer_spec.rb'
- 'ee/spec/features/projects/merge_requests/user_approves_merge_request_spec.rb'
- 'ee/spec/features/projects/settings/issues_settings_spec.rb'
- 'ee/spec/features/registrations/email_confirmation_spec.rb'

View File

@ -1 +1 @@
8efef05be30f0260bdab64a800c91ee81ffb4578
fd83dcb146ce6211929033fd73c063273f51d08d

View File

@ -1 +1 @@
d546663a59dead370600a68ecd4b79f12d6db4a0
46c6386bf37c16b85c99bc7502fd696579893b03

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

After

Width:  |  Height:  |  Size: 6.4 MiB

View File

@ -4,30 +4,34 @@ export const FREQUENTLY_USED_EMOJIS_STORAGE_KEY = 'frequently_used_emojis';
export const EMOJI_THUMBS_UP = 'thumbsup';
export const EMOJI_THUMBS_DOWN = 'thumbsdown';
/* eslint-disable @gitlab/require-i18n-strings */
export const CATEGORY_NAMES = [
FREQUENTLY_USED_KEY,
'custom',
'people',
'activity',
'nature',
'food',
'travel',
'objects',
'symbols',
'flags',
'Smileys & Emotion',
'People & Body',
'Animals & Nature',
'Food & Drink',
'Travel & Places',
'Activities',
'Objects',
'Symbols',
'Flags',
];
export const CATEGORY_ICON_MAP = {
[FREQUENTLY_USED_KEY]: 'history',
custom: 'tanuki',
activity: 'dumbbell',
people: 'smiley',
nature: 'nature',
food: 'food',
travel: 'car',
objects: 'object',
symbols: 'heart',
flags: 'flag',
'Smileys & Emotion': 'smiley',
'People & Body': 'users',
'Animals & Nature': 'nature',
'Food & Drink': 'food',
'Travel & Places': 'car',
Activities: 'dumbbell',
Objects: 'object',
Symbols: 'trigger-source',
Flags: 'flag',
};
/* eslint-enable @gitlab/require-i18n-strings */
export const EMOJIS_PER_ROW = 9;
export const EMOJI_ROW_HEIGHT = 34;

View File

@ -20,7 +20,7 @@ export const state = Vue.observable({
export const FALLBACK_EMOJI_KEY = 'grey_question';
// Keep the version in sync with `lib/gitlab/emoji.rb`
export const EMOJI_VERSION = '3';
export const EMOJI_VERSION = '4';
const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();

View File

@ -1,6 +1,6 @@
import AccessorUtilities from '~/lib/utils/accessor';
const GL_EMOJI_VERSION = '0.2.0';
const GL_EMOJI_VERSION = '0.3.0';
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
@ -25,6 +25,24 @@ const unicodeSupportTestMap = {
// angel_tone5
'\u{1F47C}\u{1F3FF}',
],
// lime, http://emojipedia.org/emoji-15.1/
15.1: '\u{1F642}\u{200D}\u{2194}\u{FE0F}',
// jellyfish, http://emojipedia.org/unicode-15.0/
'15.0': '\u{1FAE8}',
// coral, http://emojipedia.org/unicode-14.0/
'14.0': '\u{1FAE0}',
// face_with_spiral_eyes, http://emojipedia.org/emoji-13.1/
13.1: '\u{1F635}\u{200D}\u{1F4AB}',
// lungs, http://emojipedia.org/unicode-9.0/
'13.0': '\u{1FAC1}',
// person_red_hair, http://emojipedia.org/emoji-12.1/
12.1: '\u{1F9D1}\u{200D}\u{1F9B0}',
// yawning_face, http://emojipedia.org/unicode-13.0/
'12.0': '\u{1F971}',
// smiling_face_with_hearts, http://emojipedia.org/unicode-12.0/
'11.0': '\u{1F970}',
// face_vomiting, http://emojipedia.org/unicode-10.0/
'10.0': '\u{1F92E}',
// rofl, http://emojipedia.org/unicode-9.0/
'9.0': '\u{1F923}',
// metal, http://emojipedia.org/unicode-8.0/

View File

@ -262,7 +262,7 @@ export default {
<div
v-if="$scopedSlots.pagination"
class="crud-pagination gl-border-t gl-flex gl-justify-center gl-p-5"
class="crud-pagination gl-border-t gl-flex gl-justify-center gl-border-t-section gl-p-5"
data-testid="crud-pagination"
>
<slot name="pagination"></slot>

View File

@ -15,6 +15,7 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.vue';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import workItemEmailParticipantsByIidQuery from '../../graphql/notes/work_item_email_participants_by_iid.query.graphql';
const DOCS_WORK_ITEM_LOCKED_TASKS_PATH = helpPagePath('user/tasks.html', {
@ -161,6 +162,7 @@ export default {
isNoteInternal: false,
toggleResolveChecked: this.isDiscussionResolved,
emailParticipants: [],
workItem: {},
};
},
computed: {
@ -203,6 +205,12 @@ export default {
resolveCheckboxLabel() {
return this.isDiscussionResolved ? __('Unresolve thread') : __('Resolve thread');
},
canMarkNoteAsInternal() {
return this.workItem?.userPermissions?.markNoteAsInternal;
},
showInternalNoteCheckbox() {
return this.canMarkNoteAsInternal && this.isNewDiscussion;
},
},
apollo: {
emailParticipants: {
@ -227,6 +235,24 @@ export default {
);
},
},
workItem: {
query: workItemByIidQuery,
variables() {
return {
fullPath: this.fullPath,
iid: this.workItemIid,
};
},
update(data) {
return data.workspace.workItem ?? {};
},
skip() {
return !this.workItemIid;
},
error() {
this.$emit('error', i18n.fetchError);
},
},
},
methods: {
setCommentText(newText) {
@ -314,7 +340,7 @@ export default {
</label>
</div>
<gl-form-checkbox
v-if="isNewDiscussion"
v-if="showInternalNoteCheckbox"
v-model="isNoteInternal"
class="gl-mb-2"
data-testid="internal-note-checkbox"

View File

@ -589,6 +589,7 @@ export const setNewWorkItemCache = async (
setWorkItemMetadata: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
widgets,

View File

@ -34,6 +34,7 @@ fragment WorkItem on WorkItem {
setWorkItemMetadata
createNote
adminWorkItemLink
markNoteAsInternal
}
widgets {
...WorkItemWidgets

File diff suppressed because it is too large Load Diff

View File

@ -100,6 +100,7 @@ $crud-header-min-height: px-to-rem(49px);
margin-top: 0;
}
.gl-pagination ul,
.pagination {
margin-bottom: 0;
}

View File

@ -27,7 +27,7 @@
= body
- if pagination?
.crud-pagination.gl-flex.gl-justify-center.gl-p-5.gl-border-t{ data: { testid: 'crud-pagination' } }
.crud-pagination.gl-flex.gl-justify-center.gl-p-5.gl-border-t.gl-border-t-section{ data: { testid: 'crud-pagination' } }
= pagination
- if footer?

View File

@ -543,6 +543,7 @@ module ApplicationSettingsHelper
:observability_backend_ssl_verification_enabled
].tap do |settings|
unless Gitlab.com?
settings << :resource_usage_limits
settings << :deactivate_dormant_users
settings << :deactivate_dormant_users_period
settings << :nuget_skip_metadata_url_validation

View File

@ -630,6 +630,9 @@ class ApplicationSetting < ApplicationRecord
:users_get_by_id_limit
end
attribute :resource_usage_limits, :ind_jsonb, default: -> { {} }
validates :resource_usage_limits, json_schema: { filename: 'resource_usage_limits' }
jsonb_accessor :rate_limits,
concurrent_bitbucket_import_jobs_limit: [:integer, { default: 100 }],
concurrent_bitbucket_server_import_jobs_limit: [:integer, { default: 100 }],

View File

@ -169,6 +169,7 @@ module ApplicationSettingImplementation
require_admin_approval_after_user_signup: true,
require_two_factor_authentication: false,
resource_token_expiry_inherited_members: true,
resource_usage_limits: {},
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: default_min_key_size(:rsa),
session_expire_delay: Settings.gitlab['session_expire_delay'],

View File

@ -11,17 +11,22 @@ module Integrations
field :webhook,
section: SECTION_TYPE_CONNECTION,
help: 'https://chat.googleapis.com/v1/spaces…',
help: -> { _('The Hangouts Chat webhook (for example, `https://chat.googleapis.com/v1/spaces...`).') },
required: true
field :notify_only_broken_pipelines,
type: :checkbox,
section: SECTION_TYPE_CONFIGURATION
section: SECTION_TYPE_CONFIGURATION,
description: -> { _('Send notifications for broken pipelines.') }
field :branches_to_be_notified,
type: :select,
section: SECTION_TYPE_CONFIGURATION,
title: -> { s_('Integrations|Branches for which notifications are to be sent') },
description: -> {
_('Branches to send notifications for. Valid options are `all`, `default`, `protected`, ' \
'and `default_and_protected`. The default value is `default`.')
},
choices: -> { branch_choices }
def self.title

View File

@ -0,0 +1,75 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Resource usage limits",
"type": "object",
"additionalProperties": false,
"properties": {
"rules": {
"type": "array",
"minimum": 0,
"description": "List of resource usage limits",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"name": {
"type": "string",
"description": "Name of the resource usage limit"
},
"resource_key": {
"type": "string",
"description": "Lookup key to a field in the SafeRequestStore"
},
"scopes": {
"type": "array",
"description": "List of scopes for the limit",
"minimum": 1,
"items": {
"type": "string"
}
},
"metadata": {
"type": "object",
"description": "Map of metadata pertaining to the limit"
},
"rules": {
"type": "array",
"minimum": 1,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"selector": {
"type": "string",
"description": "Worker selection string, similar to Sidekiq routing"
},
"threshold": {
"type": "number",
"description": "Maximum resource usage value within the given window"
},
"interval": {
"type": "number",
"description": "Tracking window in seconds"
}
},
"required": [
"selector",
"threshold",
"interval"
]
}
}
},
"required": [
"name",
"resource_key",
"rules",
"scopes"
]
},
"required": [
"rules"
]
}
}
}

View File

@ -1,13 +1,12 @@
%h5.gl-mt-0
= _('History of authentications')
%ul.content-list
- events.each do |event|
- if event.success?
%li
%span.description
= audit_icon('key', css_class: 'gl-mr-2')
= _('Signed in with %{authentication} authentication') % { authentication: event.provider }
%span.gl-float-right= time_ago_with_tooltip(event.created_at)
= paginate events, theme: "gitlab"
= render ::Layouts::CrudComponent.new(_('History of authentications')) do |c|
- c.with_body do
%ul.content-list
- events.each do |event|
- if event.success?
%li
%span.description
= audit_icon('key', css_class: 'gl-mr-2')
= _('Signed in with %{authentication} authentication') % { authentication: event.provider }
%span.gl-float-right= time_ago_with_tooltip(event.created_at)
- c.with_pagination do
= paginate events, theme: "gitlab"

View File

@ -1,12 +1,8 @@
- page_title _('Authentication log')
- @force_desktop_expanded_sidebar = true
.settings-section.js-search-settings-section
.settings-sticky-header
.settings-sticky-header-inner
%h4.gl-my-0
= page_title
%p.gl-text-secondary
= render ::Layouts::SettingsSectionComponent.new(page_title) do |c|
- c.with_description do
= _('This is a security log of authentication events involving your account.')
= render 'event_table', events: @events
- c.with_body do
= render 'event_table', events: @events

View File

@ -0,0 +1,13 @@
---
api_type: hash
attr: resource_usage_limits
clusterwide: false
column: resource_usage_limits
db_type: jsonb
default: "'{}'::jsonb"
description: Definition for resource usage limits enforced in Sidekiq workers. This
setting is available for GitLab.com only.
encrypted: false
gitlab_com_different_than_default: true
jihu: false
not_null: true

View File

@ -5,7 +5,7 @@ clusterwide: true
column: user_seat_management
db_type: jsonb
default: "'{}'::jsonb"
description: Holds settings related to user seat cost management features.
description:
encrypted: false
gitlab_com_different_than_default: false
jihu: false

View File

@ -0,0 +1,9 @@
---
name: bulk_push_concurrency_limit_resume_worker
feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/503732
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/172553
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/504957
milestone: '17.7'
group: group::durability
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
class AddResourceUsageLimitsToApplicationSettings < Gitlab::Database::Migration[2.2]
milestone '17.7'
enable_lock_retries!
def change
add_column :application_settings, :resource_usage_limits, :jsonb, default: {}, null: false
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class RemoveSecurityPoliciesDescriptionTextLimit < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '17.7'
def up
remove_text_limit :security_policies, :description, constraint_name: 'check_99c8e08928'
end
def down
# no-op: Danger of failing if there are records with length(description) > 255
end
end

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
class RemoveOnboardingProgressesGitWriteAtColumn < Gitlab::Database::Migration[2.2]
milestone '17.6'
disable_ddl_transaction!
TABLE_NAME = :onboarding_progresses
COLUMN_NAME = :git_write_at
CREATE_INDEX = :index_onboarding_progresses_for_create_track
TEAM_INDEX = :index_onboarding_progresses_for_team_track
TRIAL_INDEX = :index_onboarding_progresses_for_trial_track
VERIFY_INDEX = :index_onboarding_progresses_for_verify_track
def up
remove_concurrent_index_by_name(TABLE_NAME, CREATE_INDEX)
remove_concurrent_index_by_name(TABLE_NAME, TEAM_INDEX)
remove_concurrent_index_by_name(TABLE_NAME, TRIAL_INDEX)
remove_concurrent_index_by_name(TABLE_NAME, VERIFY_INDEX)
remove_column(TABLE_NAME, COLUMN_NAME)
end
def down
add_column(TABLE_NAME, COLUMN_NAME, :datetime_with_timezone)
add_concurrent_index(TABLE_NAME, :created_at, where: "#{COLUMN_NAME} IS NULL", name: CREATE_INDEX)
disable_statement_timeout do
execute <<~SQL
CREATE INDEX CONCURRENTLY #{TEAM_INDEX}
ON #{TABLE_NAME}
USING btree (GREATEST(#{COLUMN_NAME}, pipeline_created_at, trial_started_at))
WHERE ((#{COLUMN_NAME} IS NOT NULL)
AND (pipeline_created_at IS NOT NULL)
AND (trial_started_at IS NOT NULL)
AND (user_added_at IS NULL))
SQL
end
disable_statement_timeout do
execute <<~SQL
CREATE INDEX CONCURRENTLY #{TRIAL_INDEX}
ON #{TABLE_NAME}
USING btree (GREATEST(#{COLUMN_NAME}, pipeline_created_at))
WHERE ((#{COLUMN_NAME} IS NOT NULL)
AND (pipeline_created_at IS NOT NULL)
AND (trial_started_at IS NULL))
SQL
end
add_concurrent_index(
TABLE_NAME,
COLUMN_NAME,
where: "(#{COLUMN_NAME} IS NOT NULL) AND (pipeline_created_at IS NULL)",
name: VERIFY_INDEX
)
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
class RemoveFileChecksumFromXrayReports < Gitlab::Database::Migration[2.2]
milestone '17.7'
def up
remove_column :xray_reports, :file_checksum
end
def down
add_column :xray_reports, :file_checksum, :binary, null: true
end
end

View File

@ -0,0 +1 @@
635883b6c6eeaa25516e0312455293e4d03a6923165d278160e9e76793d093ce

View File

@ -0,0 +1 @@
2e4df57675f9a89a8a53d2158aa7e99368deca4585409b784f6b23e12ef858c6

View File

@ -0,0 +1 @@
f5e6944be491fcffd5f59f24eb95ca527df0cafbc84ef46af4e63a00670d0043

View File

@ -0,0 +1 @@
8c7a6d45ee7385db261c5d55f7cbef5b186843da3d4e4acdcaea2a4b5dae2138

View File

@ -6899,6 +6899,7 @@ CREATE TABLE application_settings (
elasticsearch_retry_on_failure integer DEFAULT 0 NOT NULL,
integrations jsonb DEFAULT '{}'::jsonb NOT NULL,
user_seat_management jsonb DEFAULT '{}'::jsonb NOT NULL,
resource_usage_limits jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
@ -15548,7 +15549,6 @@ CREATE TABLE onboarding_progresses (
namespace_id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
git_write_at timestamp with time zone,
merge_request_created_at timestamp with time zone,
pipeline_created_at timestamp with time zone,
user_added_at timestamp with time zone,
@ -19337,8 +19337,7 @@ CREATE TABLE security_policies (
content jsonb DEFAULT '{}'::jsonb NOT NULL,
metadata jsonb DEFAULT '{}'::jsonb NOT NULL,
CONSTRAINT check_3fa0f29e4b CHECK ((char_length(name) <= 255)),
CONSTRAINT check_966e08b242 CHECK ((char_length(checksum) <= 255)),
CONSTRAINT check_99c8e08928 CHECK ((char_length(description) <= 255))
CONSTRAINT check_966e08b242 CHECK ((char_length(checksum) <= 255))
);
CREATE SEQUENCE security_policies_id_seq
@ -22185,7 +22184,6 @@ CREATE TABLE xray_reports (
updated_at timestamp with time zone NOT NULL,
lang text NOT NULL,
payload jsonb NOT NULL,
file_checksum bytea,
CONSTRAINT check_6da5a3b473 CHECK ((char_length(lang) <= 255))
);
@ -31155,14 +31153,6 @@ CREATE INDEX index_on_users_name_lower ON users USING btree (lower((name)::text)
CREATE INDEX index_on_value_stream_dashboard_aggregations_last_run_at_and_id ON value_stream_dashboard_aggregations USING btree (last_run_at NULLS FIRST, namespace_id) WHERE (enabled IS TRUE);
CREATE INDEX index_onboarding_progresses_for_create_track ON onboarding_progresses USING btree (created_at) WHERE (git_write_at IS NULL);
CREATE INDEX index_onboarding_progresses_for_team_track ON onboarding_progresses USING btree (GREATEST(git_write_at, pipeline_created_at, trial_started_at)) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NOT NULL) AND (trial_started_at IS NOT NULL) AND (user_added_at IS NULL));
CREATE INDEX index_onboarding_progresses_for_trial_track ON onboarding_progresses USING btree (GREATEST(git_write_at, pipeline_created_at)) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NOT NULL) AND (trial_started_at IS NULL));
CREATE INDEX index_onboarding_progresses_for_verify_track ON onboarding_progresses USING btree (git_write_at) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NULL));
CREATE UNIQUE INDEX index_onboarding_progresses_on_namespace_id ON onboarding_progresses USING btree (namespace_id);
CREATE INDEX index_oncall_shifts_on_rotation_id_and_starts_at_and_ends_at ON incident_management_oncall_shifts USING btree (rotation_id, starts_at, ends_at);

View File

@ -633,6 +633,7 @@ listed in the descriptions of the relevant settings.
| `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../administration/moderate_users.md) by an administrator. |
| `require_personal_access_token_expiry` | boolean | no | When enabled, users must set an expiration date when creating a group or project access token, or a personal access token owned by a non-service account. |
| `require_two_factor_authentication` | boolean | no | (**If enabled, requires:** `two_factor_grace_period`) Require all users to set up two-factor authentication. |
| `resource_usage_limits` | hash | no | Definition for resource usage limits enforced in Sidekiq workers. This setting is available for GitLab.com only. |
| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-Administrator users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is `null` which means there is no restriction.[Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot select levels that are set as `default_project_visibility` and `default_group_visibility`. |
| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |

View File

@ -7,12 +7,12 @@ info: Analysis of Application Settings for Cells 1.0.
## Statistics
- Number of attributes: 504
- Number of encrypted attributes: 43 (9.0%)
- Number of attributes documented: 310 (62.0%)
- Number of attributes on GitLab.com different from the defaults: 221 (44.0%)
- Number of attributes with `clusterwide` set: 504 (100.0%)
- Number of attributes with `clusterwide: true` set: 122 (24.0%)
- Number of attributes: 506
- Number of encrypted attributes: 43 (8.0%)
- Number of attributes documented: 311 (61.0%)
- Number of attributes on GitLab.com different from the defaults: 222 (44.0%)
- Number of attributes with `clusterwide` set: 506 (100.0%)
- Number of attributes with `clusterwide: true` set: 123 (24.0%)
## Individual columns
@ -389,6 +389,7 @@ info: Analysis of Application Settings for Cells 1.0.
| `require_personal_access_token_expiry` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` |
| `require_two_factor_authentication` | `false` | `boolean` | `boolean` | `false` | `false` | `false` | `false`| `true` |
| `required_instance_ci_template` | `false` | `text` | `` | `false` | `null` | `false` | `false`| `false` |
| `resource_usage_limits` | `false` | `jsonb` | `hash` | `true` | `'{}'::jsonb` | `true` | `false`| `true` |
| `restricted_visibility_levels` | `false` | `text` | `array of strings` | `false` | `null` | `true` | `false`| `true` |
| `rsa_key_restriction` | `false` | `integer` | `integer` | `true` | `0` | `false` | `false`| `true` |
| `runner_token_expiration_interval` | `false` | `integer` | `integer` | `false` | `null` | `false` | `false`| `true` |
@ -507,6 +508,7 @@ info: Analysis of Application Settings for Cells 1.0.
| `user_default_internal_regex` | `false` | `character` | `string` | `false` | `null` | `true` | `false`| `true` |
| `user_defaults_to_private_profile` | `false` | `boolean` | `boolean` | `true` | `false` | `false` | `false`| `true` |
| `user_oauth_applications` | `false` | `boolean` | `boolean` | `false` | `true` | `false` | `false`| `true` |
| `user_seat_management` | `false` | `jsonb` | `` | `true` | `'{}'::jsonb` | `false` | `true`| `false` |
| `user_show_add_ssh_key_message` | `false` | `boolean` | `boolean` | `true` | `true` | `false` | `false`| `true` |
| `users_get_by_id_limit` | `false` | `integer` | `` | `true` | `300` | `false` | `false`| `false` |
| `users_get_by_id_limit_allowlist` | `false` | `text[]` | `` | `true` | `'{}'::text[]` | `true` | `false`| `false` |

View File

@ -72,15 +72,16 @@ You can use the dashboard designer to:
- Add and remove visualizations.
- Resize or move panels.
## Visualization designer
## Data explorer
> - Introduced in GitLab 16.4 [with a flag](../../administration/feature_flags.md) named `combined_analytics_visualization_editor`. Disabled by default.
> - [Generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/425048) in GitLab 16.7. Feature flag `combined_analytics_visualization_editor` removed.
> - [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/470875) in GitLab 17.6 from "Visualization designer" to "Data explorer"
NOTE:
This feature is only compatible with the product analytics data source.
You can use the visualization designer to:
You can use the data explorer to:
- [Create custom visualizations](#create-a-custom-visualization).
- [Generate custom visualizations with GitLab Duo](#generate-a-custom-visualization-with-gitlab-duo).
@ -258,7 +259,7 @@ To create a custom visualization:
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Analyze > Analytics dashboards**.
1. Select **Visualization designer**.
1. Select **Data explorer**.
1. In the **Visualization title** field, enter the name of your visualization.
1. From the **Visualization type** dropdown list, select a visualization type.
1. In the **What metric do you want to visualize?** section, select a [measure or a dimension](#visualization-query-builder).
@ -285,7 +286,7 @@ To generate a custom visualization with GitLab Duo using a natural language quer
1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Analyze > Analytics dashboards**.
1. Select **Visualization designer**.
1. Select **Data explorer**.
1. In the **Visualization title** field, enter the name of your visualization.
1. From the **Visualization type** dropdown list, select a visualization type.
1. In the **Generate with GitLab Duo** section, enter your prompt. For example:
@ -353,5 +354,5 @@ If a dashboard panel displays an error message:
If GitLab Duo doesn't return the expected or a useful result, try editing your query to:
- Specify a date range. For example: _number of unique users in 2023 to 2024, grouped monthly_.
- Use the same names for metrics and dimensions as shown in the visualization designer.
- Use the same names for metrics and dimensions as shown in the data explorer.
For example: _returning users_ instead of _existing customers_.

View File

@ -1432,8 +1432,8 @@ If you're new to this, don't be <img src="https://gitlab.com/gitlab-org/gitlab-f
You can join the emoji <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/family.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":family:" alt=":family:">.
Just look up one of the supported codes.
Consult the [Emoji Cheat Sheet](https://www.webfx.com/tools/emoji-cheat-sheet/) for a list
of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/2/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":thumbsup:" alt=":thumbsup:">
Consult the [TanukiEmoji reference](https://gitlab-org.gitlab.io/ruby/gems/tanuki_emoji/) for a list
of all supported emoji codes. <img src="https://gitlab.com/gitlab-org/gitlab-foss/raw/master/public/-/emojis/4/thumbsup.png" width="20px" height="20px" style="display:inline;margin:0;border:0;padding:0;" title=":thumbsup:" alt=":thumbsup:">
The above paragraphs in raw Markdown:

View File

@ -10,10 +10,86 @@ DETAILS:
**Tier:** Premium, Ultimate
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
This page describes the syntax and error handling used in Code Owners files,
and provides an example file.
The Code Owners configuration is stored in a `CODEOWNERS` file.
This file determines who should review and approve changes.
## Code Owners syntax
This page describes the syntax and error handling used in `CODEOWNERS` files and provides examples of how to use them.
## Example `CODEOWNERS` file
```plaintext
# This is an example of a CODEOWNERS file.
# Lines that start with `#` are ignored.
# Specify a default Code Owner by using a wildcard:
* @default-codeowner
# Specify multiple Code Owners by using a tab or space:
* @multiple @code @owners
# Rules defined later in the file take precedence over earlier rules.
# For example, for all files with a filename ending in `.rb`:
*.rb @ruby-owner
# Files with a `#` can still be accessed by escaping the pound sign:
\#file_with_pound.rb @owner-file-with-pound
# You can use both usernames or email addresses to match users:
LICENSE @legal janedoe@gitlab.com
# Use group names to match groups, and nested groups:
README @group @group/with-nested/subgroup
# Specify Code Owners for directories:
/docs/ @all-docs
/docs/* @root-docs
/docs/**/*.md @root-docs
# Match directories nested anywhere in the repository:
lib/ @lib-owner
# Match only a directory in the root of the repository:
/config/ @config-owner
# If the path contains spaces, escape them like this:
path\ with\ spaces/ @space-owner
# Code Owners sections:
[Documentation]
ee/docs @docs
docs @docs
[Development] @dev-team
*
README.md @docs-team
data-models/ @data-science-team
# This section is combined with the previously defined [Documentation] section:
[DOCUMENTATION]
README.md @docs
```
## Code Owner file loading
The `CODEOWNERS` file is loaded from the target branch.
GitLab checks these locations in your repository in this order:
1. Root directory: `./CODEOWNERS`
1. Documentation directory: `./docs/CODEOWNERS`
1. `.gitlab` directory: `./.gitlab/CODEOWNERS`
The first `CODEOWNERS` file found is used, and all others are ignored.
## Pattern matching
GitLab uses `File::fnmatch` with the `File::FNM_DOTMATCH` and `File::FNM_PATHNAME` flags set for pattern matching:
- The repository structure is treated like an isolated file system.
- The patterns follow a subset of shell filename globbing rules, and are not regular expressions.
- The `File::FNM_DOTMATCH` flag allows `*` to match dotfiles like `.gitignore`.
- The `File::FNM_PATHNAME` flag prevents `*` from matching the `/` path separator.
- `**` matches directories recursively. For example, `**/*.rb` matches `config/database.rb`
and `app/controllers/users/stars_controller.rb`.
### Comments
@ -25,7 +101,7 @@ Lines beginning with `#` are ignored:
### Sections
Sections are groups of entries. A section begins with a section heading in square brackets, followed by the entries.
Sections are groups of entries. A section begins with a section heading in square brackets `[ ]`:
```plaintext
[Section name]
@ -35,8 +111,14 @@ Sections are groups of entries. A section begins with a section heading in squar
#### Section headings
Section headings must always have a name. They can also be made optional, or
require a specific number of approvals. A list of default owners can be added to the section heading line.
Section headings must have a name and can:
- Require approval (default).
- Be optional (prefixed with `^`).
- Require a specific number of approvals. For more information, see [Group inheritance and eligibility](index.md#group-inheritance-and-eligibility) and [Approvals shown as optional](troubleshooting.md#approvals-shown-as-optional).
- Include default owners.
Examples:
```plaintext
# Required section
@ -57,91 +139,56 @@ require a specific number of approvals. A list of default owners can be added to
#### Section names
Sections names are defined between square brackets. Section names are not case-sensitive.
Section names are case-insensitive and defined between square brackets.
[Sections with duplicate names](index.md#sections-with-duplicate-names) are combined.
```plaintext
[Section name]
```
#### Required sections
Required sections do not include `^` before the [section name](#section-names).
```plaintext
[Required section]
```
#### Optional sections
Optional sections include a `^` before the [section name](#section-names).
```plaintext
^[Optional section]
```
#### Sections requiring multiple approvals
Sections requiring multiple approvals include the number of approvals in square brackets after the [section name](#section-names).
```plaintext
[Section requiring 5 approvals][5]
```
NOTE:
Optional sections ignore the number of approvals required.
#### Sections with default owners
You can define a default owner for the entries in a section by appending the owners to the [section heading](#section-headings).
```plaintext
# Section with @username as default owner
[Section name] @username
# Section with @group and @subgroup as default owners and requiring 2 approvals
[Section name][2] @group @subgroup
```
### Code Owner entries
Each Code Owner entry includes a path followed by one or more owners.
Each entry includes a path followed by one or more owners.
```plaintext
README.md @username1
README.md @username1 @username2
```
NOTE:
If an entry is duplicated in a section, [the last entry is used from each section.](index.md#define-more-specific-owners-for-more-specifically-defined-files-or-directories)
If an entry is duplicated in a section, [the last entry is used.](index.md#define-more-specific-owners-for-more-specifically-defined-files-or-directories)
## Path matching
Paths can be absolute, relative, wildcard, or globstar, and are matched against the repository root.
### Relative paths
If a path does not start with a `/`, the path is treated as if it starts with
a [globstar](#globstar-paths). `README.md` is treated the same way as `/**/README.md`:
Paths without a leading `/` are treated as [globstar paths](#globstar-paths):
```plaintext
# This will match /README.md, /internal/README.md, /app/lib/README.md
# Matches /README.md, /internal/README.md, /app/lib/README.md
README.md @username
# This will match /internal/README.md, /docs/internal/README.md, /docs/api/internal/README.md
# Matches /internal/README.md, /docs/internal/README.md, /docs/api/internal/README.md
internal/README.md
```
NOTE:
When using globstar paths, be cautious of unintended matches.
For example, `README.md` without a leading `/` matches any `README.md`
file in any directory or subdirectory of the repository.
### Absolute paths
If a path starts with a `/` it matches the root of the repository.
Paths starting with `/` match from the repository root:
```plaintext
# Matches only the file named `README.md` in the root of the repository.
# # Matches only README.md in the root.
/README.md
# Matches only the file named `README.md` inside the `/docs` directory.
# Matches only README.md inside the /docs directory.
/docs/README.md
```
### Directory paths
If a path ends with `/`, the path matches any file in the directory.
Paths ending with `/` match any file in the directory:
```plaintext
# This is the same as `/docs/**/*`
@ -150,7 +197,7 @@ If a path ends with `/`, the path matches any file in the directory.
### Wildcard paths
Wildcards can be used to match one of more characters of a path.
Use wildcards to match multiple characters:
```plaintext
# Any markdown files in the docs directory
@ -171,17 +218,17 @@ Wildcards can be used to match one of more characters of a path.
### Globstar paths
Globstars (`**`) can be used to match zero or more directories and subdirectories.
Use `**` to match zero or more directories recursively:
```plaintext
# This will match /docs/index.md, /docs/api/index.md, /docs/api/graphql/index.md
# Matches /docs/index.md, /docs/api/index.md, and /docs/api/graphql/index.md.
/docs/**/index.md
```
### Entry owners
## Entry owners
Entries must be followed by one or more owner. These can be groups, subgroups,
and users. Order of owners is not important.
Entries must have one or more owners These can be groups, subgroups,
and users.
```plaintext
/path/to/entry.rb @group
@ -190,38 +237,21 @@ and users. Order of owners is not important.
/path/to/entry.rb @group @group/subgroup @user
```
#### Groups as entry owners
For more information on adding groups as Code Owners, see [Add a group as a Code Owner](index.md#add-a-group-as-a-code-owner).
Groups and subgroups can be owners of an entry.
Each entry can be owned by [one or more owners](#entry-owners).
For more details see the [Add a group as a Code Owner](index.md#add-a-group-as-a-code-owner).
```plaintext
/path/to/entry.rb @group
/path/to/entry.rb @group/subgroup
/path/to/entry.rb @group @group/subgroup
```
### Users as entry owners
Users can be owners of an entry. Each entry can be owned by
[one or more owners](#entry-owners).
```plaintext
/path/to/entry.rb @username1
/path/to/entry.rb @username1 @username2
```
## Error handling in Code Owners
## Error handling
> - Error validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216066) in GitLab 16.3.
### Entries with spaces
Paths containing whitespace must be escaped with backslashes: `path\ with\ spaces/*.md`.
Without the backslashes, the path after the first whitespace is parsed as an owner.
GitLab the parses `folder with spaces/*.md @group` into
`path: "folder", owners: " with spaces/*.md @group"`.
Escape whitespace in paths with backslashes:
```plaintext
path\ with\ spaces/*.md @owner
```
Without escaping, GitLab parses `folder with spaces/*.md @group` as: `path: "folder", owners: " with spaces/*.md @group"`.
### Unparsable sections
@ -265,22 +295,23 @@ GitLab recognizes the heading `[Section name` as an entry. The `[Docs]` section
### Malformed owners
Each entry must contain 1 or more owners to be valid, malformed owners are ignored.
For example `/path/* @group user_without_at_symbol @user_with_at_symbol`
is owned by `@group` and `@user_with_at_symbol`.
Each entry must contain one or more owners. Malformed owners are invalid and ignored:
```plaintext
/path/* @group user_without_at_symbol @user_with_at_symbol
```
This entry is owned by `@group` and `@user_with_at_symbol`.
### Inaccessible or incorrect owners
Inaccessible or incorrect owners are ignored. For example, if `@group`, `@username`,
and `example@gitlab.com` are accessible on the project and we create an entry:
GitLab ignores inaccessible or incorrect owners. For example:
```plaintext
* @group @grou @username @i_left @i_dont_exist example@gitlab.com invalid@gitlab.com
```
GitLab ignores `@grou`, `@i_left`, `@i_dont_exist`, and `invalid@gitlab.com`.
For more information on who is accessible, see [Add a group as a Code Owner](index.md#add-a-group-as-a-code-owner).
If only `@group`, `@username`, and `example@gitlab.com` are accessible, GitLab ignores the others.
### Zero owners
@ -292,85 +323,14 @@ NOTE:
When a protected branch has `Require code owner approval` enabled, rules with
zero owners are still honored.
### Less than 1 required approval
### Minimum approvals
When [defining the number of approvals](index.md#require-multiple-approvals-from-code-owners) for a section,
the minimum number of approvals is `1`. Setting the number of approvals to
`0` results in GitLab requiring one approval.
## Example `CODEOWNERS` file
## Related topics
```plaintext
# This is an example of a CODEOWNERS file.
# Lines that start with `#` are ignored.
# app/ @commented-rule
# Specify a default Code Owner by using a wildcard:
* @default-codeowner
# Specify multiple Code Owners by using a tab or space:
* @multiple @code @owners
# Rules defined later in the file take precedence over the rules
# defined before.
# For example, for all files with a filename ending in `.rb`:
*.rb @ruby-owner
# Files with a `#` can still be accessed by escaping the pound sign:
\#file_with_pound.rb @owner-file-with-pound
# Specify multiple Code Owners separated by spaces or tabs.
# In the following case the CODEOWNERS file from the root of the repo
# has 3 Code Owners (@multiple @code @owners):
CODEOWNERS @multiple @code @owners
# You can use both usernames or email addresses to match
# users. Everything else is ignored. For example, this code
# specifies the `@legal` and a user with email `janedoe@gitlab.com` as the
# owner for the LICENSE file:
LICENSE @legal this_does_not_match janedoe@gitlab.com
# Use group names to match groups, and nested groups to specify
# them as owners for a file:
README @group @group/with-nested/subgroup
# End a path in a `/` to specify the Code Owners for every file
# nested in that directory, on any level:
/docs/ @all-docs
# End a path in `/*` to specify Code Owners for every file in
# a directory, but not nested deeper. This code matches
# `docs/index.md` but not `docs/projects/index.md`:
/docs/* @root-docs
# Include `/**` to specify Code Owners for all subdirectories
# in a directory. This rule matches `docs/projects/index.md` or
# `docs/development/index.md`
/docs/**/*.md @root-docs
# This code makes matches a `lib` directory nested anywhere in the repository:
lib/ @lib-owner
# This code match only a `config` directory in the root of the repository:
/config/ @config-owner
# If the path contains spaces, escape them like this:
path\ with\ spaces/ @space-owner
# Code Owners section:
[Documentation]
ee/docs @docs
docs @docs
# Use of default owners for a section. In this case, all files (*) are owned by
the dev team except the README.md and data-models which are owned by other teams.
[Development] @dev-team
*
README.md @docs-team
data-models/ @data-science-team
# This section is combined with the previously defined [Documentation] section:
[DOCUMENTATION]
README.md @docs
```
- [Code Owners](index.md)
- [Merge request approvals](../merge_requests/approvals/index.md)
- [Protected branches](../repository/branches/protected.md)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -121,20 +121,7 @@ module API
chat_notification_channels
].flatten,
'google-play' => ::Integrations::GooglePlay.api_arguments,
'hangouts-chat' => [
{
required: true,
name: :webhook,
type: String,
desc: 'The Hangouts Chat webhook. e.g. https://chat.googleapis.com/v1/spaces…'
},
{
required: false,
name: :branches_to_be_notified,
type: String,
desc: 'Branches for which notifications are to be sent'
}
].flatten,
'hangouts-chat' => ::Integrations::HangoutsChat.api_arguments,
'harbor' => ::Integrations::Harbor.api_arguments,
'irker' => [
{

View File

@ -237,6 +237,7 @@ module API
optional :downstream_pipeline_trigger_limit_per_project_user_sha, type: Integer, desc: 'Maximum number of downstream pipelines that can be triggered per minute (for a given project, user, and commit).'
optional :ai_action_api_rate_limit, type: Integer, desc: 'Maximum requests a user can make per 8 hours to aiAction endpoint'
optional :code_suggestions_api_rate_limit, type: Integer, desc: 'Maximum requests a user can make per minute to code suggestions endpoint'
optional :resource_usage_limits, type: JSON, desc: 'Definition for resource usage limits enforced in Sidekiq workers'
Gitlab::SSHPublicKey.supported_types.each do |type|
optional :"#{type}_key_restriction",

View File

@ -44,6 +44,8 @@ module Gitlab
end
end
private
# Creates the merge request and returns its ID.
#
# This method will return `nil` if the merge request could not be

View File

@ -27,7 +27,7 @@ module Gitlab
metrics_upsert(gitlab_user_id)
if mapper.user_mapping_enabled?
push_with_record(merge_request.metrics, :merged_by_id, merged_by[:id], mapper.user_mapper)
push_with_record(merge_request.metrics, :merged_by_id, merged_by.id, mapper.user_mapper)
else
add_legacy_note!
end

View File

@ -32,6 +32,8 @@ module Gitlab
add_approval!(gitlab_user_id)
add_reviewer!(gitlab_user_id) if options[:add_reviewer]
else
# TODO this method and this if/else can be removed when `github_user_mapping` flag is removed
# because there will always be a gitlab_user_id when using placeholder users.
add_complementary_review_note!(project.creator_id)
end
end

View File

@ -75,10 +75,18 @@ module Gitlab
validate_unescaped_chars(errors, entry)
validate_html(errors, entry)
validate_translation(errors, entry)
validate_namespace(errors, entry)
errors
end
def validate_namespace(errors, entry)
if entry.translations_contain_namespace?
errors << 'contains a namespace, remove it from the translation. For more information see ' \
'https://docs.gitlab.com/ee/development/i18n/translation.html#namespaced-strings'
end
end
def validate_unescaped_chars(errors, entry)
if entry.msgid_contains_unescaped_chars?
errors << 'contains unescaped `%`, escape it using `%%`'

View File

@ -5,6 +5,7 @@ module Gitlab
class TranslationEntry
PERCENT_REGEX = /(?:^|[^%])%(?!{\w*}|[a-z%])/
ANGLE_BRACKET_REGEX = /[<>]/
NAMESPACE_REGEX = /^((?u)\w+|\s)*\|/
attr_reader :nplurals, :entry_data
@ -68,6 +69,14 @@ module Gitlab
translation_entries.any?(Array)
end
def translations_contain_namespace?
all_translations.any? { |translation| contains_namespace?(translation) }
end
def contains_namespace?(string)
string =~ NAMESPACE_REGEX
end
def msgid_contains_unescaped_chars?
contains_unescaped_chars?(msgid)
end

View File

@ -77,23 +77,17 @@ module Gitlab
end
def create_approval!(project_id, merge_request_id, user_id, submitted_at)
approval_attributes = {
approval = Approval.create(
merge_request_id: merge_request_id,
user_id: user_id,
created_at: submitted_at,
updated_at: submitted_at
}
approval_result = ::Approval.insert(
approval_attributes,
returning: [:id],
unique_by: [:user_id, :merge_request_id]
updated_at: submitted_at,
importing: true
)
return unless approval_result.rows.present?
return unless approval.persisted?
note = add_approval_system_note!(project_id, merge_request_id, user_id, submitted_at)
approval = Approval.find(approval_result.first['id'])
[approval, note]
end

View File

@ -57,6 +57,8 @@ module Gitlab
# eg: `config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)`
def self.client_configurator
->(chain) do
# ConcurrencyLimit::Resume needs to be first and before Labkit and ConcurrencyLimit::Client
chain.add ::Gitlab::SidekiqMiddleware::ConcurrencyLimit::Resume
chain.add ::Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add ::Labkit::Middleware::Sidekiq::Client
# Sidekiq Client Middleware should be placed before DuplicateJobs::Client middleware,

View File

@ -6,7 +6,8 @@ module Gitlab
class ConcurrencyLimitService
REDIS_KEY_PREFIX = 'sidekiq:concurrency_limit'
delegate :add_to_queue!, :queue_size, :has_jobs_in_queue?, :resume_processing!, to: :@queue_manager
delegate :add_to_queue!, :queue_size, :metadata_key, :has_jobs_in_queue?, :resume_processing!,
to: :@queue_manager
delegate :track_execution_start, :track_execution_end, :cleanup_stale_trackers,
:concurrent_worker_count, to: :@worker_execution_tracker
@ -34,6 +35,10 @@ module Gitlab
new(worker_name).queue_size
end
def metadata_key(worker_name)
new(worker_name).metadata_key
end
def cleanup_stale_trackers(worker_name)
new(worker_name).cleanup_stale_trackers
end

View File

@ -7,12 +7,14 @@ module Gitlab
include ExclusiveLeaseGuard
LEASE_TIMEOUT = 10.seconds
RESUME_PROCESSING_BATCH_SIZE = 1_000
attr_reader :redis_key, :worker_name
attr_reader :redis_key, :metadata_key, :worker_name
def initialize(worker_name:, prefix:)
@worker_name = worker_name
@redis_key = "#{prefix}:throttled_jobs:{#{worker_name.underscore}}"
@metadata_key = "#{prefix}:resume_meta:{#{worker_name.underscore}}"
end
def add_to_queue!(args, context)
@ -37,7 +39,7 @@ module Gitlab
jobs = next_batch_from_queue(redis, limit: limit)
break if jobs.empty?
jobs.each { |job| send_to_processing_queue(deserialize(job)) }
send_or_bulk_send_to_processing_queue(jobs)
remove_processed_jobs(redis, limit: jobs.length)
jobs.length
@ -67,19 +69,63 @@ module Gitlab
Gitlab::Json.parse(json)
end
def send_or_bulk_send_to_processing_queue(jobs)
if Feature.enabled?(:bulk_push_concurrency_limit_resume_worker, :current_request)
bulk_send_to_processing_queue(jobs)
else
jobs.each { |job| send_to_processing_queue(deserialize(job)) }
end
end
def send_to_processing_queue(job)
context = job['context'] || {}
Gitlab::ApplicationContext.with_raw_context(context) do
args = job['args']
Gitlab::SidekiqLogging::ConcurrencyLimitLogger.instance.resumed_log(worker_name, args)
worker_klass = worker_name.safe_constantize
next if worker_klass.nil?
worker_klass.concurrency_limit_resume(job['buffered_at']).perform_async(*args)
end
end
def bulk_send_to_processing_queue(jobs)
return if worker_klass.nil?
jobs.each_slice(RESUME_PROCESSING_BATCH_SIZE) do |batch|
args_list = prepare_and_store_metadata(batch)
Gitlab::SidekiqLogging::ConcurrencyLimitLogger.instance.resumed_log(worker_name, args_list)
worker_klass.bulk_perform_async(args_list) # rubocop:disable Scalability/BulkPerformWithContext -- context is set separately in SidekiqMiddleware::ConcurrencyLimit::Resume
end
end
def prepare_and_store_metadata(jobs)
queue = Queue.new
args_list = []
jobs.map! do |job|
deserialized = deserialize(job)
queue.push(job_metadata(deserialized))
args_list << deserialized['args']
end
# Since bulk_perform_async doesn't support updating job payload one by one,
# we'll rely on Gitlab::SidekiqMiddleware::ConcurrencyLimit::Resume client middleware
# to update each job with the required metadata.
Gitlab::SafeRequestStore.write(metadata_key, queue)
args_list
end
def job_metadata(job)
{
'concurrency_limit_buffered_at' => job['buffered_at'],
'concurrency_limit_resume' => true
}.merge(job['context'])
end
def worker_klass
worker_name.safe_constantize
end
def next_batch_from_queue(redis, limit:)
return [] unless limit > 0

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Gitlab
module SidekiqMiddleware
module ConcurrencyLimit
class Resume
EmptyJobMetadataError = Class.new(StandardError)
# This middleware updates the job payload with stored context and additional information needed by the
# SidekiqMiddleware::ConcurrencyLimit::Middleware to resume the job.
def call(worker_class_or_name, job, _queue, _redis_pool, &)
return yield if Feature.disabled?(:bulk_push_concurrency_limit_resume_worker, :current_request)
worker_name = worker_class_or_name.is_a?(Class) ? worker_class_or_name.name : worker_class_or_name
return yield unless worker_name
# metadata is written in QueueManager#bulk_send_to_processing_queue
metadata_queue = Gitlab::SafeRequestStore[metadata_key(worker_name)]
return yield if metadata_queue.nil?
if metadata_queue.empty?
Gitlab::ErrorTracking.track_exception(
EmptyJobMetadataError.new("Missing job metadata from ConcurrencyLimit::ResumeWorker"))
return yield
end
job.merge!(metadata_queue.pop)
yield
end
def metadata_key(worker_name)
Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitService.metadata_key(worker_name)
end
end
end
end
end

View File

@ -6458,6 +6458,9 @@ msgstr ""
msgid "Analytics|Dashboards are created by editing the projects dashboard files."
msgstr ""
msgid "Analytics|Data explorer"
msgstr ""
msgid "Analytics|Data table"
msgstr ""
@ -6608,7 +6611,7 @@ msgstr ""
msgid "Analytics|Use ClickHouse for supported analytics reports. Available when %{link_start}ClickHouse is configured%{link_end}."
msgstr ""
msgid "Analytics|Use the visualization designer to create custom visualizations. After you save a visualization, you can add it to a dashboard."
msgid "Analytics|Use the data explorer to create custom visualizations. After you save a visualization, you can add it to a dashboard."
msgstr ""
msgid "Analytics|View available dashboards"
@ -6620,9 +6623,6 @@ msgstr ""
msgid "Analytics|Visualization"
msgstr ""
msgid "Analytics|Visualization designer"
msgstr ""
msgid "Analytics|Visualization title"
msgstr ""
@ -55217,6 +55217,9 @@ msgstr ""
msgid "The GitLab user to which the Jira user %{jiraDisplayName} will be mapped"
msgstr ""
msgid "The Hangouts Chat webhook (for example, `https://chat.googleapis.com/v1/spaces...`)."
msgstr ""
msgid "The ID of the application."
msgstr ""

View File

@ -157,7 +157,7 @@
"dompurify": "^3.2.0",
"dropzone": "^4.2.0",
"editorconfig": "^0.15.3",
"emoji-regex": "^10.0.0",
"emoji-regex": "^10.3.0",
"fast-mersenne-twister": "1.0.2",
"file-loader": "^6.2.0",
"fuzzaldrin-plus": "^0.6.0",

View File

@ -198,6 +198,7 @@ class ApplicationSettingsAnalysis
repository_storages_weighted
require_admin_approval_after_user_signup
require_admin_two_factor_authentication
resource_usage_limits
restricted_visibility_levels
runners_registration_token
runners_registration_token_encrypted

View File

@ -10,7 +10,7 @@ ee/spec/frontend/admin/subscriptions/show/components/subscription_details_histor
ee/spec/frontend/ai/settings/components/duo_experiment_beta_features_form_spec.js
ee/spec/frontend/analytics/analytics_dashboards/components/analytics_dashboard_panel_spec.js
ee/spec/frontend/analytics/analytics_dashboards/components/analytics_dashboards_breadcrumbs_spec.js
ee/spec/frontend/analytics/analytics_dashboards/components/analytics_visualization_designer_spec.js
ee/spec/frontend/analytics/analytics_dashboards/components/analytics_data_explorer_spec.js
ee/spec/frontend/analytics/analytics_dashboards/components/list/feature_list_item_spec.js
ee/spec/frontend/analytics/cycle_analytics/components/value_stream_aggregation_status_spec.js
ee/spec/frontend/analytics/cycle_analytics/components/value_stream_form_content_spec.js

25
spec/fixtures/namespaces.po vendored Normal file
View File

@ -0,0 +1,25 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the gitlab package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"Language-Team: Spanish\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
msgid "404|Not found"
msgstr "404|No encontrado"
msgid "CommitHistory|1 commit"
msgid_plural "CommitHistory|%d commits"
msgstr[0] "CommitHistory|1 cambio"
msgstr[1] "CommitHistory|%d cambios"

View File

@ -1131,3 +1131,9 @@ msgid "CycleAnalytics|%{stageName}"
msgid_plural "CycleAnalytics|%d stages selected"
msgstr[0] "%{stageName}"
msgstr[1] "%d stages selected"
msgid "Example: (feature|hotfix)\\/.*"
msgstr "Ejemplo: (feature|hotfix)\\/.*"
msgid "Example: (jar|exe)$"
msgstr "Ejemplo: (jar|exe)$"

View File

@ -8,6 +8,12 @@ export const validEmoji = {
unicodeVersion: '4.1',
aliases: ['atom_symbol'],
},
fast_reverse_button: {
moji: '⏪',
description: 'fast reverse button',
unicodeVersion: '4.1',
aliases: ['rewind'],
},
bomb: {
moji: '💣',
unicodeVersion: '6.0',
@ -16,17 +22,17 @@ export const validEmoji = {
construction_worker_tone5: {
moji: '👷🏿',
unicodeVersion: '8.0',
description: 'construction worker tone 5',
description: 'construction worker: dark skin tone',
},
five: {
moji: '5⃣',
unicodeVersion: '3.0',
description: 'keycap digit five',
description: 'keycap: 5',
},
grey_question: {
moji: '❔',
unicodeVersion: '6.0',
description: 'white question mark ornament',
description: 'white question mark',
},
black_heart: {
moji: '🖤',
@ -36,7 +42,7 @@ export const validEmoji = {
heart: {
moji: '❤',
unicodeVersion: '1.1',
description: 'heavy black heart',
description: 'red heart',
},
custard: {
moji: '🍮',
@ -46,7 +52,7 @@ export const validEmoji = {
star: {
moji: '⭐',
unicodeVersion: '5.1',
description: 'white medium star',
description: 'star',
},
gay_pride_flag: {
moji: '🏳️‍🌈',
@ -61,13 +67,78 @@ export const validEmoji = {
thumbsup: {
moji: '👍',
unicodeVersion: '6.0',
description: 'thumbs up sign',
description: 'thumbs up',
},
thumbsdown: {
moji: '👎',
description: 'thumbs down sign',
description: 'thumbs down',
unicodeVersion: '6.0',
},
expressionless: {
moji: '😑',
description: 'expressionless face',
unicodeVersion: '6.1',
},
spy: {
moji: '🕵️',
description: 'detective',
unicodeVersion: '7.0',
},
metal: {
moji: '🤘',
description: 'sign of the horns',
unicodeVersion: '8.0',
},
rofl: {
moji: '🤣',
description: 'rolling on the floor laughing',
unicodeVersion: '9.0',
},
face_vomiting: {
moji: '🤮',
description: 'face vomiting',
unicodeVersion: '10.0',
},
man_superhero: {
moji: '🦸‍♂️',
description: 'man superhero',
unicodeVersion: '11.0',
},
person_standing: {
moji: '🧍',
description: 'person standing',
unicodeVersion: '12.0',
},
person_red_hair: {
moji: '🧑‍🦰',
description: 'person: red hair',
unicodeVersion: '12.1',
},
people_hugging: {
moji: '🫂',
description: 'people hugging',
unicodeVersion: '13.0',
},
face_with_spiral_eyes: {
moji: '😵‍💫',
description: 'face with spiral eyes',
unicodeVersion: '13.1',
},
coral: {
moji: '🪸',
description: 'coral',
unicodeVersion: '14.0',
},
jellyfish: {
moji: '🪼',
description: 'jellyfish',
unicodeVersion: '15.0',
},
lime: {
moji: '🍋‍🟩',
description: 'lime',
unicodeVersion: '15.1',
},
};
export const invalidEmoji = {

View File

@ -16,65 +16,65 @@ describe('AwardsHandler', () => {
const emojiData = [
{
n: '8ball',
c: 'activity',
c: 'Activities',
e: '🎱',
d: 'billiards',
d: 'pool 8 ball',
u: '6.0',
},
{
n: 'grinning',
c: 'people',
c: 'Smileys & Emotion',
e: '😀',
d: 'grinning face',
u: '6.1',
},
{
n: 'angel',
c: 'people',
c: 'Smileys & Emotion',
e: '👼',
d: 'baby angel',
u: '6.0',
},
{
n: 'anger',
c: 'symbols',
c: 'Smileys & Emotion',
e: '💢',
d: 'anger symbol',
u: '6.0',
},
{
n: 'alien',
c: 'people',
c: 'Smileys & Emotion',
e: '👽',
d: 'extraterrestrial alien',
d: 'alien',
u: '6.0',
},
{
n: 'sunglasses',
c: 'people',
c: 'Smileys & Emotion',
e: '😎',
d: 'smiling face with sunglasses',
u: '6.0',
},
{
n: 'grey_question',
c: 'symbols',
c: 'Symbols',
e: '❔',
d: 'white question mark ornament',
d: 'white question mark',
u: '6.0',
},
{
n: EMOJI_THUMBS_UP,
c: 'people',
c: 'People & Body',
e: '👍',
d: 'thumbs up sign',
d: 'thumbs up',
u: '6.0',
},
{
n: EMOJI_THUMBS_DOWN,
c: 'people',
c: 'People & Body',
e: '👎',
d: 'thumbs down sign',
d: 'thumbs down',
u: '6.0',
},
];

View File

@ -21,10 +21,7 @@ describe('content_editor/extensions/emoji', () => {
describe('when typing a valid emoji input rule', () => {
it('inserts an emoji node', () => {
const expectedDoc = doc(
p(
' ',
emoji({ moji: '❤', name: 'heart', title: 'heavy black heart', unicodeVersion: '1.1' }),
),
p(' ', emoji({ moji: '❤', name: 'heart', title: 'red heart', unicodeVersion: '1.1' })),
);
triggerNodeInputRule({ tiptapEditor, inputRuleText: ':heart:' });

View File

@ -51,6 +51,15 @@ const emptySupportMap = {
horseRacing: false,
flag: false,
skinToneModifier: false,
15.1: false,
'15.0': false,
'14.0': false,
13.1: false,
'13.0': false,
12.1: false,
'12.0': false,
'11.0': false,
'10.0': false,
'9.0': false,
'8.0': false,
'7.0': false,
@ -605,6 +614,163 @@ describe('emoji', () => {
expect(isSupported).toBe(false);
});
it('expressionless(6.1)', () => {
const emojiKey = 'expressionless';
const unicodeSupportMap = { ...emptySupportMap, 6.1: true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('spy(7.0)', () => {
const emojiKey = 'spy';
const unicodeSupportMap = { ...emptySupportMap, '7.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('metal(8.0)', () => {
const emojiKey = 'metal';
const unicodeSupportMap = { ...emptySupportMap, '8.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('rofl(9.0)', () => {
const emojiKey = 'rofl';
const unicodeSupportMap = { ...emptySupportMap, '9.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('face_vomiting(10.0)', () => {
const emojiKey = 'face_vomiting';
const unicodeSupportMap = { ...emptySupportMap, '10.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('man superhero(11.0)', () => {
const emojiKey = 'man_superhero';
const unicodeSupportMap = { ...emptySupportMap, '11.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('person standing(12.0)', () => {
const emojiKey = 'person_standing';
const unicodeSupportMap = { ...emptySupportMap, '12.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('person: red hair(12.1)', () => {
const emojiKey = 'person_red_hair';
const unicodeSupportMap = { ...emptySupportMap, 12.1: true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('people hugging(13.0)', () => {
const emojiKey = 'people_hugging';
const unicodeSupportMap = { ...emptySupportMap, '13.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('face_with_spiral_eyes(13.1)', () => {
const emojiKey = 'face_with_spiral_eyes';
const unicodeSupportMap = { ...emptySupportMap, 13.1: true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('coral(14.0)', () => {
const emojiKey = 'coral';
const unicodeSupportMap = { ...emptySupportMap, '14.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('jellyfish(15.0)', () => {
const emojiKey = 'jellyfish';
const unicodeSupportMap = { ...emptySupportMap, '15.0': true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('lime(15.1)', () => {
gon.emoji_backend_version = null;
const emojiKey = 'lime';
const unicodeSupportMap = { ...emptySupportMap, 15.1: true };
const isSupported = isEmojiUnicodeSupported(
unicodeSupportMap,
emojiFixtureMap[emojiKey].moji,
emojiFixtureMap[emojiKey].unicodeVersion,
);
expect(isSupported).toBe(true);
});
it('construction_worker_tone5(8.0) without skin tone modifier support', () => {
const emojiKey = 'construction_worker_tone5';
const unicodeSupportMap = {
@ -816,12 +982,12 @@ describe('emoji', () => {
],
[
'searching by partial description',
'ment',
'sym',
[
{
name: 'grey_question',
name: 'atom',
field: 'd',
score: 16777216,
score: 32,
},
],
],
@ -863,7 +1029,7 @@ describe('emoji', () => {
[
{
name: 'star',
field: 'name',
field: 'd',
score: NEUTRAL_INTENT_MULTIPLIER,
},
{
@ -892,7 +1058,6 @@ describe('emoji', () => {
])('should return a correct result when %s', (_, query, searchResult) => {
const expected = searchResult.map((item) => {
const { field, score, name } = item;
return {
emoji: getEmojiMap()[name],
field,

View File

@ -28,7 +28,7 @@ describe('Unicode Support Map', () => {
expect(getArgs[1][0]).toBe('gl-emoji-user-agent');
expect(setArgs[0][0]).toBe('gl-emoji-version');
expect(setArgs[0][1]).toBe('0.2.0');
expect(setArgs[0][1]).toBe('0.3.0');
expect(setArgs[1][0]).toBe('gl-emoji-user-agent');
expect(setArgs[1][1]).toBe(navigator.userAgent);
expect(setArgs[2][0]).toBe('gl-emoji-unicode-support-map');

View File

@ -978,7 +978,7 @@ describe('GfmAutoComplete', () => {
'atwho-at': ':',
emoji: {
c: 'symbols',
d: 'negative squared ab',
d: 'AB button (blood type)',
e: '🆎',
name: 'ab',
u: '6.0',

View File

@ -70,7 +70,7 @@ exports[`Merge request dashboard collapsible section renders section 1`] = `
content
</div>
<div
class="crud-pagination gl-border-t gl-flex gl-justify-center gl-p-5"
class="crud-pagination gl-border-t gl-border-t-section gl-flex gl-justify-center gl-p-5"
data-testid="crud-pagination"
/>
</div>

View File

@ -153,7 +153,7 @@ describe('EmojiToken', () => {
it('renders token item when value is selected', () => {
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbs_up"
expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbsup"
expect(tokenSegments.at(2).findComponent(GlEmoji).attributes('data-name')).toEqual(
EMOJI_THUMBS_UP,
);

View File

@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as autosave from '~/lib/utils/autosave';
import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
import { STATE_OPEN, i18n } from '~/work_items/constants';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import workItemEmailParticipantsByIidQuery from '~/work_items/graphql/notes/work_item_email_participants_by_iid.query.graphql';
import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
@ -17,6 +18,7 @@ import WorkItemStateToggle from '~/work_items/components/work_item_state_toggle.
import {
workItemEmailParticipantsResponse,
workItemEmailParticipantsEmptyResponse,
workItemByIidResponseFactory,
} from '../../mock_data';
Vue.use(VueApollo);
@ -36,6 +38,8 @@ const workItemId = 'gid://gitlab/WorkItem/1';
describe('Work item comment form component', () => {
let wrapper;
let workItemResponse;
let workItemResponseHandler;
const mockAutosaveKey = 'test-auto-save-key';
@ -66,11 +70,19 @@ describe('Work item comment form component', () => {
isDiscussionResolved = false,
isDiscussionResolvable = false,
hasEmailParticipantsWidget = false,
canMarkNoteAsInternal = true,
emailParticipantsResponseHandler = emailParticipantsSuccessHandler,
} = {}) => {
workItemResponse = workItemByIidResponseFactory({
canMarkNoteAsInternal,
});
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
wrapper = shallowMountExtended(WorkItemCommentForm, {
apolloProvider: createMockApollo([
[workItemEmailParticipantsByIidQuery, emailParticipantsResponseHandler],
[workItemByIidQuery, workItemResponseHandler],
]),
propsData: {
fullPath: 'test-project-path',
@ -335,8 +347,25 @@ describe('Work item comment form component', () => {
});
describe('when used as a new discussion', () => {
describe('user permissions to mark note as internal', () => {
it('should have the ability to add internal note when permission exists', async () => {
createComponent({ canMarkNoteAsInternal: true, isNewDiscussion: true });
await waitForPromises();
expect(findInternalNoteCheckbox().exists()).toBe(true);
});
it('should not have the ability to add internal note when permission does not exist', async () => {
createComponent({ canMarkNoteAsInternal: false, isNewDiscussion: true });
await waitForPromises();
expect(findInternalNoteCheckbox().exists()).toBe(false);
});
});
beforeEach(() => {
createComponent({ isNewDiscussion: true });
return waitForPromises();
});
it('should have the add as internal note capability', () => {

View File

@ -207,6 +207,7 @@ export const workItemQueryResponse = {
adminParentLink: false,
createNote: false,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
widgets: [
@ -323,6 +324,7 @@ export const updateWorkItemMutationResponse = {
adminParentLink: false,
createNote: false,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@ -453,6 +455,7 @@ export const convertWorkItemMutationResponse = {
adminParentLink: false,
createNote: false,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
reference: 'gitlab-org/gitlab-test#1',
@ -1161,6 +1164,7 @@ export const workItemResponseFactory = ({
canCreateNote = false,
adminParentLink = false,
canAdminWorkItemLink = true,
canMarkNoteAsInternal = true,
notificationsWidgetPresent = true,
currentUserTodosWidgetPresent = true,
awardEmojiWidgetPresent = true,
@ -1243,6 +1247,7 @@ export const workItemResponseFactory = ({
adminParentLink,
adminWorkItemLink: canAdminWorkItemLink,
createNote: canCreateNote,
markNoteAsInternal: canMarkNoteAsInternal,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@ -1611,6 +1616,7 @@ export const createWorkItemMutationResponse = {
adminParentLink: false,
createNote: false,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@ -1683,6 +1689,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
adminParentLink: false,
createNote: false,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
namespace: {
@ -2051,6 +2058,7 @@ export const workItemHierarchyResponse = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
author: {
@ -2114,6 +2122,7 @@ export const workItemObjectiveWithChild = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
author: {
@ -2170,6 +2179,7 @@ export const workItemObjectiveWithoutChild = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
author: {
@ -2222,6 +2232,7 @@ export const workItemHierarchyTreeEmptyResponse = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@ -2532,6 +2543,7 @@ export const workItemHierarchyTreeResponse = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@ -2575,6 +2587,7 @@ export const workItemHierarchyTreeSingleClosedItemResponse = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@ -2711,6 +2724,7 @@ export const workItemObjectiveWithClosedChild = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
author: {
@ -2776,6 +2790,7 @@ export const changeWorkItemParentMutationResponse = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
description: null,
@ -5203,6 +5218,7 @@ export const createWorkItemQueryResponse = {
setWorkItemMetadata: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
widgets: [
@ -5491,6 +5507,7 @@ const mockUserPermissions = {
setWorkItemMetadata: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
};
@ -5647,6 +5664,7 @@ export const workItemHierarchyNoChildrenTreeResponse = {
adminParentLink: true,
createNote: true,
adminWorkItemLink: true,
markNoteAsInternal: true,
__typename: 'WorkItemPermissions',
},
confidential: false,

View File

@ -38,7 +38,7 @@ describe('Integration Snippets notes', () => {
':heart',
['heart', 'heart decoration', 'heart with arrow', 'heart with ribbon', 'heart_exclamation'],
],
[':red', ['red apple', 'red_car', 'red_circle', 'credit card', 'tired face']],
[':red', ['red apple', 'red_car', 'red_circle', 'red_exclamation_mark', 'red_heart']],
[
':circle',
// TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/347549

View File

@ -50,6 +50,10 @@ RSpec.describe ApplicationSettingsHelper do
expect(helper.visible_attributes).to include(*%i[snowplow_collector_hostname snowplow_cookie_domain snowplow_enabled snowplow_app_id])
end
it 'contains :resource_usage_limits' do
expect(helper.visible_attributes).to include(:resource_usage_limits)
end
it 'contains :deactivate_dormant_users' do
expect(helper.visible_attributes).to include(:deactivate_dormant_users)
end

View File

@ -5,11 +5,11 @@ require 'spec_helper'
RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redis_shared_state, feature_category: :importers do
include Import::UserMappingHelper
let_it_be(:imported_from) { ::Import::HasImportSource::IMPORT_SOURCES[:github] }
let_it_be(:imported_from) { Import::HasImportSource::IMPORT_SOURCES[:github] }
let_it_be(:user_representation_1) { Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice') }
let_it_be(:user_representation_2) { Gitlab::GithubImport::Representation::User.new(id: 5, login: 'bob') }
let_it_be_with_reload(:project) do
create(:project, :repository, :with_import_url, :import_user_mapping_enabled, import_type: ::Import::SOURCE_GITHUB)
create(:project, :repository, :with_import_url, :import_user_mapping_enabled, import_type: Import::SOURCE_GITHUB)
end
let_it_be(:source_user_1) do
@ -17,7 +17,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
:import_source_user,
source_user_identifier: user_representation_1.id,
source_hostname: project.import_url,
import_type: ::Import::SOURCE_GITHUB,
import_type: Import::SOURCE_GITHUB,
namespace: project.root_ancestor
)
end
@ -27,7 +27,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
:import_source_user,
source_user_identifier: user_representation_2.id,
source_hostname: project.import_url,
import_type: ::Import::SOURCE_GITHUB,
import_type: Import::SOURCE_GITHUB,
namespace: project.root_ancestor
)
end
@ -45,8 +45,8 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
let(:description) { 'This is my pull request' }
let(:state) { :closed }
let(:pull_request) do
Gitlab::GithubImport::Representation::PullRequest.new(
let(:pull_request_attributes) do
{
iid: 42,
title: 'My Pull Request',
description: description,
@ -64,16 +64,18 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
created_at: created_at,
updated_at: updated_at,
merged_at: state == :closed && merged_at
)
}
end
let(:pull_request) { Gitlab::GithubImport::Representation::PullRequest.new(pull_request_attributes) }
let(:importer) { described_class.new(pull_request, project, client) }
describe '#execute' do
describe '#execute', :aggregate_failures do
it 'imports the pull request and assignees' do
expect(importer).to receive(:insert_git_data)
expect { importer.execute }.to change { MergeRequest.count }.by(1)
expect { importer.execute }.to change { MergeRequest.count }.from(0).to(1)
created_merge_request = MergeRequest.last
created_mr_assignees = created_merge_request.assignees
@ -94,7 +96,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
author_id: source_user_1.mapped_user_id,
created_at: created_at,
updated_at: updated_at,
imported_from: ::Import::SOURCE_GITHUB.to_s
imported_from: Import::SOURCE_GITHUB.to_s
)
end
@ -123,7 +125,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
it 'pushes placeholder references to the store' do
importer.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
user_references = placeholder_user_references(Import::SOURCE_GITHUB, project.import_state.id)
created_merge_request = MergeRequest.last
created_mr_assignee = created_merge_request.merge_request_assignees.first # we only import one PR assignee
@ -133,333 +135,228 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitla
])
end
context 'when user mapping is disabled' do
before do
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: false })
allow(importer.user_finder).to receive(:user_id_for).and_return(nil)
end
context 'when the description has user mentions' do
let(:description) { 'You can ask @knejad by emailing xyz@gitlab.com' }
it 'does not push any placeholder references' do
it 'adds backticks to the username' do
importer.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
expect(user_references).to be_empty
expect(MergeRequest.last.description).to eq("You can ask `@knejad` by emailing xyz@gitlab.com")
end
end
end
describe '#create_merge_request' do
before do
allow(importer.milestone_finder)
.to receive(:id_for)
.with(pull_request)
.and_return(milestone.id)
context 'when the pull request does not have assignees' do
it 'creates a merge request without assignees' do
pull_request_attributes[:assignee] = nil
expect { importer.execute }.to change { MergeRequest.count }.from(0).to(1)
created_merge_request = MergeRequest.last
expect(created_merge_request.assignees).to be_empty
end
end
it 'creates a merge request with placeholder author' do
expect { importer.create_merge_request }.to change { MergeRequest.count }.from(0).to(1)
context 'when the source and target branch are identical' do
it 'uses a generated source branch name for the merge request' do
pull_request_attributes[:source_repository_id] = pull_request_attributes[:target_repository_id]
pull_request_attributes[:source_branch] = pull_request_attributes[:target_branch]
created_merge_request = MergeRequest.last
importer.execute
expect(created_merge_request.author_id).to eq(source_user_1.mapped_user_id)
expect(MergeRequest.last.source_branch).to eq('master-42')
end
end
context 'when the merge request is invalid' do
it 'does not create a duplicate merge request when it has already been created' do
expect { 2.times { importer.execute } }.to change { MergeRequest.count }.from(0).to(1)
end
it 'skips creating a merge request without error when a foreign key error is raised' do
allow(importer).to receive(:insert_and_return_id)
.and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
expect { importer.execute }.not_to change { MergeRequest.count }
end
it 'raises all other exceptions and does not create a merge request' do
allow(pull_request).to receive(:formatted_source_branch).and_return(nil)
expect { importer.execute }.to raise_error(ActiveRecord::RecordInvalid)
.and not_change { MergeRequest.count }
end
end
context 'with git data' do
before do
allow(importer.milestone_finder)
.to receive(:id_for)
.with(pull_request)
.and_return(milestone.id)
end
it 'does not create the source branch if merge request is merged' do
importer.execute
mr = MergeRequest.last
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
it 'creates a merge request diff and sets it as the latest' do
importer.execute
mr = MergeRequest.last
expect(mr.merge_request_diffs.exists?).to eq(true)
expect(mr.reload.latest_merge_request_diff_id).to eq(mr.merge_request_diffs.first.id)
end
it 'creates the merge request diff commits' do
importer.execute
mr = MergeRequest.last
diff = mr.merge_request_diffs.reload.first
expect(diff.merge_request_diff_commits.exists?).to eq(true)
end
context 'when merge request is open' do
let(:project) { create(:project, :repository, :with_import_url, :import_user_mapping_enabled) }
let(:state) { :opened }
it 'creates the source branch' do
importer.execute
mr = MergeRequest.last
expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
it 'is able to retry on pre-receive errors' do
expect(importer).to receive(:insert_or_replace_git_data).twice.and_call_original
allow(project.repository).to receive(:add_branch).and_raise('exception')
expect { importer.execute }.to raise_error('exception')
expect(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original
importer.execute
mr = MergeRequest.last
expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
expect(mr.merge_request_diffs).to be_one
end
it 'ignores Git command errors when creating a branch' do
allow(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError)
expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
importer.execute
mr = MergeRequest.last
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
it 'ignores Git PreReceive errors when creating a branch' do
allow(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::PreReceiveError)
expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
importer.execute
mr = MergeRequest.last
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
end
context 'when the merge request exists' do
it 'creates the merge request diffs if they do not yet exist' do
importer.execute
mr = MergeRequest.last
mr.merge_request_diff.destroy!
importer.execute
expect(mr.merge_request_diffs.exists?).to eq(true)
end
end
end
context 'when user mapping is disabled' do
context 'when the author could be found' do
before do
allow(importer.user_finder)
.to receive(:author_id_for)
.with(pull_request)
.and_return([user.id, true])
end
it 'imports the pull request with the pull request author as the merge request author' do
expect(importer)
.to receive(:insert_and_return_id)
.with(
{
iid: 42,
title: 'My Pull Request',
description: 'This is my pull request',
source_project_id: project.id,
target_project_id: project.id,
source_branch: 'github/fork/alice/feature',
target_branch: 'master',
state_id: 3,
milestone_id: milestone.id,
author_id: user.id,
created_at: created_at,
updated_at: updated_at,
imported_from: imported_from
},
project.merge_requests
)
.and_call_original
importer.create_merge_request
end
it 'returns the created merge request' do
mr, exists = importer.create_merge_request
expect(mr).to be_instance_of(MergeRequest)
expect(exists).to eq(false)
end
context 'when the description has user mentions' do
let(:description) { 'You can ask @knejad by emailing xyz@gitlab.com' }
it 'adds backticks to the username' do
expect(importer).to receive(:insert_and_return_id).with(
a_hash_including(description: "You can ask `@knejad` by emailing xyz@gitlab.com"),
project.merge_requests
).and_call_original
importer.create_merge_request
end
end
context 'when the source and target branch are identical' do
before do
allow(pull_request).to receive_messages(
source_repository_id: pull_request.target_repository_id,
source_branch: 'master'
)
end
it 'uses a generated source branch name for the merge request' do
expect(importer)
.to receive(:insert_and_return_id)
.with(
{
iid: 42,
title: 'My Pull Request',
description: 'This is my pull request',
source_project_id: project.id,
target_project_id: project.id,
source_branch: 'master-42',
target_branch: 'master',
state_id: 3,
milestone_id: milestone.id,
author_id: user.id,
created_at: created_at,
updated_at: updated_at,
imported_from: imported_from
},
project.merge_requests
)
.and_call_original
importer.create_merge_request
end
end
context 'when the import fails due to a foreign key error' do
it 'does not raise any errors' do
expect(importer)
.to receive(:insert_and_return_id)
.and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key')
expect { importer.create_merge_request }.not_to raise_error
end
end
context 'when the merge request already exists' do
it 'returns the existing merge request' do
mr1, exists1 = importer.create_merge_request
mr2, exists2 = importer.create_merge_request
expect(mr2).to eq(mr1)
expect(exists1).to eq(false)
expect(exists2).to eq(true)
end
end
end
context 'when the author could not be found' do
before do
allow(importer.user_finder)
.to receive(:author_id_for)
.with(pull_request)
.and_return([project.creator_id, false])
end
it 'imports the pull request with the project creator as the merge request author' do
expect(importer)
.to receive(:insert_and_return_id)
.with(
{
iid: 42,
title: 'My Pull Request',
description: "*Created by: alice*\n\nThis is my pull request",
source_project_id: project.id,
target_project_id: project.id,
source_branch: 'github/fork/alice/feature',
target_branch: 'master',
state_id: 3,
milestone_id: milestone.id,
author_id: project.creator_id,
created_at: created_at,
updated_at: updated_at,
imported_from: imported_from
},
project.merge_requests
)
.and_call_original
importer.create_merge_request
end
end
end
context 'when merge request is invalid' do
before do
allow(pull_request).to receive(:formatted_source_branch).and_return(nil)
allow(importer.user_finder)
.to receive(:author_id_for)
.with(pull_request)
.and_return([project.creator_id, false])
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: false }).save!
end
it 'fails validation' do
expect { importer.create_merge_request }.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
context 'when author and assignee are found' do
let_it_be(:user_2) { create(:user) }
describe '#set_merge_request_assignees' do
let_it_be(:merge_request) { create(:merge_request) }
before do
allow(importer.user_finder)
.to receive(:find)
.with(user_representation_1.id, user_representation_1.login)
.and_return(user.id)
before do
allow(importer.user_finder)
.to receive(:assignee_id_for)
.with(pull_request)
.and_return(user_id)
allow(importer.user_finder)
.to receive(:find)
.with(user_representation_2.id, user_representation_2.login)
.and_return(user_2.id)
end
importer.set_merge_request_assignees(merge_request)
end
it 'imports the merge request with gitlab matching gitlab author and assignee' do
expect { importer.execute }.to change { MergeRequest.count }.from(0).to(1)
.and not_change { User.where(user_type: :placeholder).count }
context 'when pull request has an assignee' do
let(:user_id) { user.id }
created_merge_request = MergeRequest.last
it 'sets merge request assignees' do
expect(merge_request.assignee_ids).to eq [user.id]
end
end
expect(created_merge_request.author.id).to eq(user.id)
expect(created_merge_request.assignees.first.id).to eq(user_2.id) # we only import one PR assignee
end
context 'when pull request does not have any assignees' do
let(:user_id) { nil }
it 'does not push any placeholder references' do
importer.execute
it 'does not set merge request assignees' do
expect(merge_request.assignee_ids).to eq []
end
end
end
user_references = placeholder_user_references(Import::SOURCE_GITHUB, project.import_state.id)
describe '#insert_git_data' do
before do
allow(importer.milestone_finder)
.to receive(:id_for)
.with(pull_request)
.and_return(milestone.id)
allow(importer.user_finder)
.to receive(:author_id_for)
.with(pull_request)
.and_return([user.id, true])
end
it 'does not create the source branch if merge request is merged' do
mr = insert_git_data
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
context 'when merge request is open' do
let(:project) { create(:project, :repository) }
let(:state) { :opened }
it 'creates the source branch' do
# Ensure the project creator is creating the branches because the
# merge request author may not have access to push to this
# repository. The project owner may also be a group.
allow(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original
mr = insert_git_data
expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
expect(user_references).to be_empty
end
end
it 'is able to retry on pre-receive errors' do
expect(importer).to receive(:insert_or_replace_git_data).twice.and_call_original
expect(project.repository).to receive(:add_branch).and_raise('exception')
context 'when author and assignee are not found' do
before do
allow(importer.user_finder)
.to receive(:find)
.with(user_representation_1.id, user_representation_1.login)
.and_return(nil)
expect { insert_git_data }.to raise_error('exception')
allow(importer.user_finder)
.to receive(:find)
.with(user_representation_2.id, user_representation_2.login)
.and_return(nil)
end
expect(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original
it 'imports the merge request with the project creator as the author' do
expect { importer.execute }.to change { MergeRequest.count }.from(0).to(1)
.and not_change { User.where(user_type: :placeholder).count }
mr = insert_git_data
expect(MergeRequest.last.author.id).to eq(project.creator_id)
end
expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
expect(mr.merge_request_diffs).to be_one
it 'does not assign assignees that were not found' do
expect { importer.execute }.not_to change { MergeRequestAssignee.count }
end
it 'does not push any placeholder references' do
importer.execute
user_references = placeholder_user_references(Import::SOURCE_GITHUB, project.import_state.id)
expect(user_references).to be_empty
end
end
it 'ignores Git command errors when creating a branch' do
expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError)
expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
mr = insert_git_data
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
it 'ignores Git PreReceive errors when creating a branch' do
expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::PreReceiveError)
expect(Gitlab::ErrorTracking).to receive(:track_exception).and_call_original
mr = insert_git_data
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
end
it 'creates a merge request diff and sets it as the latest' do
mr = insert_git_data
expect(mr.merge_request_diffs.exists?).to eq(true)
expect(mr.reload.latest_merge_request_diff_id).to eq(mr.merge_request_diffs.first.id)
end
it 'creates the merge request diff commits' do
mr = insert_git_data
diff = mr.merge_request_diffs.reload.first
expect(diff.merge_request_diff_commits.exists?).to eq(true)
end
context 'when the merge request exists' do
it 'creates the merge request diffs if they do not yet exist' do
mr, _ = importer.create_merge_request
mr.merge_request_diffs.delete_all
importer.insert_git_data(mr, true)
expect(mr.merge_request_diffs.exists?).to eq(true)
end
end
def insert_git_data
mr, exists = importer.create_merge_request
importer.insert_git_data(mr, exists)
mr
end
end
end

View File

@ -6,7 +6,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::MergedByImporter, :
include Import::UserMappingHelper
let_it_be(:project) do
create(:project, :with_import_url, :import_user_mapping_enabled, import_type: ::Import::SOURCE_GITHUB)
create(:project, :with_import_url, :import_user_mapping_enabled, import_type: Import::SOURCE_GITHUB)
end
let_it_be(:merge_request) { create(:merged_merge_request, project: project) }
@ -15,7 +15,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::MergedByImporter, :
:import_source_user,
source_user_identifier: 999,
source_hostname: project.import_url,
import_type: ::Import::SOURCE_GITHUB,
import_type: Import::SOURCE_GITHUB,
namespace: project.root_ancestor
)
end
@ -57,7 +57,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::MergedByImporter, :
it 'pushes placeholder references to the store' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
user_references = placeholder_user_references(Import::SOURCE_GITHUB, project.import_state.id)
metrics = merge_request.metrics.reload
expect(user_references).to match_array([

View File

@ -6,8 +6,8 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
include Import::UserMappingHelper
using RSpec::Parameterized::TableSyntax
let_it_be(:project) do
create(:project, :with_import_url, :import_user_mapping_enabled, import_type: ::Import::SOURCE_GITHUB)
let_it_be_with_reload(:project) do
create(:project, :with_import_url, :import_user_mapping_enabled, import_type: Import::SOURCE_GITHUB)
end
let_it_be(:source_user) do
@ -15,7 +15,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
:import_source_user,
source_user_identifier: 999,
source_hostname: project.import_url,
import_type: ::Import::SOURCE_GITHUB,
import_type: Import::SOURCE_GITHUB,
namespace: project.root_ancestor
)
end
@ -30,6 +30,8 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
)
end
let(:user_references) { placeholder_user_references(Import::SOURCE_GITHUB, project.import_state.id) }
before do
allow(client_double).to receive_message_chain(:octokit, :last_response, :headers).and_return({ etag: nil })
end
@ -109,8 +111,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'pushes placeholder references for reviewer and system note' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_approval = merge_request.approvals.last
created_reviewer = merge_request.merge_request_reviewers.last
system_note = merge_request.notes.last
@ -136,8 +136,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'only pushes placeholder references for reviewer' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_reviewer = merge_request.merge_request_reviewers.last
expect(user_references).to match_array([
@ -159,8 +157,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'only pushes placeholder references for reviewer' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_reviewer = merge_request.merge_request_reviewers.last
expect(user_references).to match_array([
@ -181,8 +177,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'only pushes placeholder references for reviewer' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_reviewer = merge_request.merge_request_reviewers.last
expect(user_references).to match_array([
@ -217,8 +211,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'pushes placeholder references for reviewer, system note, and reviewer note' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_approval = merge_request.approvals.last
created_reviewer = merge_request.merge_request_reviewers.last
system_note = merge_request.notes.where(system: true).last
@ -251,8 +243,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'pushes placeholder references for reviewer and reviewer note' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_reviewer = merge_request.merge_request_reviewers.last
note = merge_request.notes.last
@ -281,8 +271,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
it 'pushes placeholder references for reviewer and reviewer note' do
subject.execute
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
created_reviewer = merge_request.merge_request_reviewers.last
note = merge_request.notes.last
@ -297,11 +285,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewImporter, :cl
context 'when user mapping is disabled' do
before_all do
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: false })
end
after(:all) do
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: true })
project.build_or_assign_import_data(data: { user_contribution_mapping_enabled: false }).save!
end
context 'when the review author can be mapped to a gitlab user' do

View File

@ -12,7 +12,7 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImport
:import_source_user,
source_user_identifier: 1,
source_hostname: project.import_url,
import_type: ::Import::SOURCE_GITHUB,
import_type: Import::SOURCE_GITHUB,
namespace: project.root_ancestor
)
end
@ -29,6 +29,8 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImport
)
end
let(:user_references) { placeholder_user_references(Import::SOURCE_GITHUB, project.import_state.id) }
subject(:importer) { described_class.new(review_request, project, client) }
it 'imports unique merge request reviewers as placeholders', :aggregate_failures do
@ -37,15 +39,13 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImport
reviewers = merge_request.reviewers
expect(reviewers.size).to eq(2)
expect(reviewers.all?(&:placeholder?)).to be(true)
expect(reviewers).to all(be_placeholder)
expect(reviewers).to include(source_user.mapped_user)
end
it 'pushes placeholder references for unique merge request reviewers' do
expect { 2.times { importer.execute } }.not_to raise_error
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
# The existing placeholder will always have a lower id than the one created during import
# so we can assume the first reviewer is for source_user when sorted by user_id
created_reviewers = merge_request.merge_request_reviewers.sort_by(&:user_id)
@ -74,8 +74,6 @@ RSpec.describe Gitlab::GithubImport::Importer::PullRequests::ReviewRequestImport
end
it 'does not push any placeholder references' do
user_references = placeholder_user_references(::Import::SOURCE_GITHUB, project.import_state.id)
expect(user_references).to be_empty
end
end

View File

@ -47,6 +47,24 @@ RSpec.describe Gitlab::I18n::PoLinter do
end
end
context 'for a translations with namespaces' do
let(:po_path) { 'spec/fixtures/namespaces.po' }
it 'has an error for translation with namespace' do
message_id = "404|Not found"
expected_message = "contains a namespace, remove it from the translation. For more information see https://docs.gitlab.com/ee/development/i18n/translation.html#namespaced-strings"
expect(errors[message_id]).to include(expected_message)
end
it 'has an error for plural translation with namespace' do
message_id = "CommitHistory|1 commit"
expected_message = "contains a namespace, remove it from the translation. For more information see https://docs.gitlab.com/ee/development/i18n/translation.html#namespaced-strings"
expect(errors[message_id]).to include(expected_message)
end
end
context 'for a translations with newlines' do
let(:po_path) { 'spec/fixtures/newlines.po' }
@ -214,6 +232,7 @@ RSpec.describe Gitlab::I18n::PoLinter do
expect(linter).to receive(:validate_number_of_plurals).with([], fake_entry)
expect(linter).to receive(:validate_unescaped_chars).with([], fake_entry)
expect(linter).to receive(:validate_translation).with([], fake_entry)
expect(linter).to receive(:validate_namespace).with([], fake_entry)
expect(linter).to receive(:validate_html).with([], fake_entry)
linter.validate_entry(fake_entry)

View File

@ -133,6 +133,140 @@ RSpec.describe Gitlab::I18n::TranslationEntry do
end
end
describe '#translations_contain_namespace' do
it 'is true when the msgstr contains namespace' do
data = {
msgid: '404|Not found',
msgstr: '404|No encontrado'
}
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_contain_namespace?).to be_truthy
end
it 'is true when one plural translation contains namespace' do
data = {
msgid: 'Test|hello world',
msgid_plural: 'Test|hello worlds',
"msgstr[0]" => 'Test|hello world',
"msgstr[1]" => 'hello worlds'
}
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_contain_namespace?).to be_truthy
end
it 'is true when all plural translation contains namespace' do
data = {
msgid: 'Test|hello world',
msgid_plural: 'Test|hello worlds',
"msgstr[0]" => 'Test|hello world',
"msgstr[1]" => 'Test|hello worlds'
}
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_contain_namespace?).to be_truthy
end
it "is false when the msgstr doesn't contain namespace" do
data = {
msgid: '404|Not found',
msgstr: 'No encontrado'
}
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_contain_namespace?).to be_falsy
end
it "is false when the msgstr contains namespace but source is from list of false positives" do
data = {
msgid: 'Example: (jar|exe)$',
msgstr: 'Ejemplo: (jar|exe)$'
}
entry = described_class.new(entry_data: data, nplurals: 2)
data_two = {
msgid: 'Example: (feature|hotfix)\\\\/.*',
msgstr: 'Ejemplo: (feature|hotfix)\\\\/.*'
}
entry_two = described_class.new(entry_data: data_two, nplurals: 2)
expect(entry_two.translations_contain_namespace?).to be_falsy
expect(entry.translations_contain_namespace?).to be_falsy
end
it 'is false when no plural translation contains namespace' do
data = {
msgid: 'Test|hello world',
msgid_plural: 'Test|hello worlds',
"msgstr[0]" => 'hello world',
"msgstr[1]" => 'hello worlds'
}
entry = described_class.new(entry_data: data, nplurals: 2)
expect(entry.translations_contain_namespace?).to be_falsy
end
end
describe '#contains_namespace' do
let(:data) { { msgid: '' } }
let(:entry) { described_class.new(entry_data: data, nplurals: 2) }
it 'is true when the string contains a namespace' do
string = '404|Not found'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is true when the string contains a namespace with leading spaces' do
string = ' 404|Not found'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is true when the string contains a namespace with trailing spaces' do
string = '404 |Not found'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is true when the string contains a namespace with leading and trailing spaces' do
string = ' 404 |Not found'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is true when the string contains a namespace with a space' do
string = '40 4|Not found'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is true when the string contains a namespace with unicode characters' do
string = 'ZobrazeníČasu|System'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is true when the string contains a namespace with unicode characters and a space' do
string = 'Zobrazení Času|System'
expect(entry.contains_namespace?(string)).to be_truthy
end
it 'is false when the string contains a pipe, but not a namespace' do
string = 'Example: (jar|exe)$'
expect(entry.contains_namespace?(string)).to be_falsy
end
it 'is false when the string does not contain a namespace' do
string = 'Not found'
expect(entry.contains_namespace?(string)).to be_falsy
end
end
describe '#contains_unescaped_chars' do
let(:data) { { msgid: '' } }
let(:entry) { described_class.new(entry_data: data, nplurals: 2) }

View File

@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Gitlab::SidekiqMiddleware::ConcurrencyLimit::QueueManager,
:clean_gitlab_redis_shared_state, feature_category: :global_search do
:clean_gitlab_redis_shared_state, :request_store, feature_category: :global_search do
let(:worker_class) do
Class.new do
def self.name
@ -75,61 +75,124 @@ RSpec.describe Gitlab::SidekiqMiddleware::ConcurrencyLimit::QueueManager,
let(:jobs) { [[1], [2], [3]] }
let(:setter) { instance_double('Sidekiq::Job::Setter') }
let(:buffered_at) { Time.now.utc }
let(:metadata_queue) do
queue = Queue.new
2.times do
queue.push({ 'concurrency_limit_buffered_at' => buffered_at.to_f,
'concurrency_limit_resume' => true }.merge(stored_context))
end
queue
end
let(:metadata_key) { service.metadata_key }
subject(:stored_metadata_queue) { Gitlab::SafeRequestStore.read(metadata_key) }
before do
service.remove_instance_variable(:@lease) if service.instance_variable_defined?(:@lease)
travel_to(buffered_at) do
jobs.each do |j|
service.add_to_queue!(j, worker_context)
end
end
end
it 'puts jobs back into the queue and respects order' do
travel_to(buffered_at) do
jobs.each do |j|
service.add_to_queue!(j, worker_context)
end
end
expect_next_instance_of(Gitlab::ExclusiveLease) do |el|
expect(el).to receive(:try_obtain).and_call_original
end
expect(worker_class).to receive(:concurrency_limit_resume)
.with(a_value_within(1).of(buffered_at.to_f)).twice.and_return(setter)
expect(setter).to receive(:perform_async).with(1).ordered
expect(setter).to receive(:perform_async).with(2).ordered
expect(setter).not_to receive(:perform_async).with(3).ordered
expect(Gitlab::SidekiqLogging::ConcurrencyLimitLogger.instance)
.to receive(:resumed_log)
.with(worker_class_name, [1])
expect(Gitlab::SidekiqLogging::ConcurrencyLimitLogger.instance)
.to receive(:resumed_log)
.with(worker_class_name, [2])
.with(worker_class_name, [[1], [2]])
expect(Gitlab::SafeRequestStore).to receive(:write).with(
metadata_key,
kind_of(Queue)
).and_call_original
expect(worker_class).to receive(:bulk_perform_async).with([[1], [2]])
service.resume_processing!(limit: 2)
until metadata_queue.empty?
expected = metadata_queue.pop
actual = stored_metadata_queue.pop
expect(actual['concurrency_limit_buffered_at']).to be_within(1.second)
.of(expected['concurrency_limit_buffered_at'])
expect(actual.except('concurrency_limit_buffered_at')).to eq(expected.except('concurrency_limit_buffered_at'))
end
expect(stored_metadata_queue).to be_empty
end
it 'drops a set after execution' do
travel_to(buffered_at) do
jobs.each do |j|
service.add_to_queue!(j, worker_context)
end
end
expect_next_instance_of(Gitlab::ExclusiveLease) do |el|
expect(el).to receive(:try_obtain).and_call_original
end
expect(Gitlab::ApplicationContext).to receive(:with_raw_context)
.with(stored_context)
.exactly(jobs.count).times.and_call_original
expect(worker_class).to receive(:concurrency_limit_resume)
.with(a_value_within(1).of(buffered_at.to_f)).exactly(3).times.and_return(setter)
expect(setter).to receive(:perform_async).exactly(jobs.count).times
expect(worker_class).to receive(:bulk_perform_async).with([[1], [2], [3]])
expect { service.resume_processing!(limit: jobs.count) }
.to change { service.has_jobs_in_queue? }.from(true).to(false)
end
context 'when processing more than batch size' do
before do
stub_const("#{described_class}::RESUME_PROCESSING_BATCH_SIZE", 1)
end
it 'pushes the jobs in batches' do
jobs.each do |args|
expect(worker_class).to receive(:bulk_perform_async).with([args]).ordered
end
service.resume_processing!(limit: jobs.count)
end
end
context 'when bulk_push_concurrency_limit_resume_worker FF is disabled' do
before do
stub_feature_flags(bulk_push_concurrency_limit_resume_worker: false)
end
it 'puts jobs back into the queue and respects order' do
expect_next_instance_of(Gitlab::ExclusiveLease) do |el|
expect(el).to receive(:try_obtain).and_call_original
end
expect(worker_class).to receive(:concurrency_limit_resume)
.with(a_value_within(1).of(buffered_at.to_f)).twice.and_return(setter)
expect(setter).to receive(:perform_async).with(1).ordered
expect(setter).to receive(:perform_async).with(2).ordered
expect(setter).not_to receive(:perform_async).with(3).ordered
expect(Gitlab::SidekiqLogging::ConcurrencyLimitLogger.instance)
.to receive(:resumed_log)
.with(worker_class_name, [1])
expect(Gitlab::SidekiqLogging::ConcurrencyLimitLogger.instance)
.to receive(:resumed_log)
.with(worker_class_name, [2])
service.resume_processing!(limit: 2)
end
it 'drops a set after execution' do
expect_next_instance_of(Gitlab::ExclusiveLease) do |el|
expect(el).to receive(:try_obtain).and_call_original
end
expect(Gitlab::ApplicationContext).to receive(:with_raw_context)
.with(stored_context)
.exactly(jobs.count).times.and_call_original
expect(worker_class).to receive(:concurrency_limit_resume)
.with(a_value_within(1).of(buffered_at.to_f)).exactly(3).times.and_return(setter)
expect(setter).to receive(:perform_async).exactly(jobs.count).times
expect { service.resume_processing!(limit: jobs.count) }
.to change { service.has_jobs_in_queue? }.from(true).to(false)
end
end
context 'when exclusive lease is already being held' do
before do
service.exclusive_lease.try_obtain

View File

@ -0,0 +1,145 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::SidekiqMiddleware::ConcurrencyLimit::Resume, :request_store, feature_category: :scalability do
let(:resumed_worker_class) do
Class.new do
def self.name
'TestResumedWorker'
end
include ApplicationWorker
feature_category :scalability
def perform(*args); end
end
end
let(:normal_worker_class) do
Class.new do
def self.name
'TestNormalWorker'
end
include ApplicationWorker
feature_category :scalability
def perform(*args); end
end
end
let(:buffered_at) { 1.minute.ago.utc }
let(:stored_jobs_metadata) do
[
{ 'concurrency_limit_buffered_at' => buffered_at.to_f, 'concurrency_limit_resume' => true }.merge(context),
{ 'concurrency_limit_buffered_at' => (buffered_at + 1.second).to_f, 'concurrency_limit_resume' => true }
.merge(context)
]
end
let(:context) do
{
"#{Gitlab::ApplicationContext::LOG_KEY}.project" => 'gitlab-org/gitlab',
"correlation_id" => 'context_correlation_id'
}
end
let(:metadata_key) do
Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitService.metadata_key(TestResumedWorker.name)
end
before do
stub_const(resumed_worker_class.name, resumed_worker_class)
stub_const(normal_worker_class.name, normal_worker_class)
queue = Queue.new
stored_jobs_metadata.each { |jm| queue.push(jm) }
Gitlab::SafeRequestStore.write(metadata_key, queue)
end
describe '#call' do
context 'with normal worker' do
it 'does not contain any metadata from request store' do
TestNormalWorker.perform_async
job = TestNormalWorker.jobs.last
expect(job).not_to include(
"#{Gitlab::ApplicationContext::LOG_KEY}.project",
'concurrency_limit_resume',
'concurrency_limit_buffered_at'
)
expect(job['correlation_id']).not_to eq('context_correlation_id')
end
end
context 'with resumed worker' do
it 'contains metadata from request store' do
TestResumedWorker.perform_async
job = TestResumedWorker.jobs.last
expect(job['correlation_id']).to eq('context_correlation_id')
expect(job["#{Gitlab::ApplicationContext::LOG_KEY}.project"]).to eq('gitlab-org/gitlab')
expect(job['concurrency_limit_resume']).to be(true)
expect(job['concurrency_limit_buffered_at']).to eq(buffered_at.to_f)
end
it 'pops the metadata from request store' do
expect { TestResumedWorker.perform_async }.to change { Gitlab::SafeRequestStore.read(metadata_key).size }
.from(2).to(1)
end
context 'when metadata queue is empty' do
before do
allow(Gitlab::ErrorTracking).to receive(:track_exception)
end
it 'tracks an exception with missing metadata' do
3.times { TestResumedWorker.perform_async }
expect(Gitlab::ErrorTracking).to have_received(:track_exception).with(
described_class::EmptyJobMetadataError
)
end
end
end
end
context 'as integration test with QueueManager#resume_processing!' do
let(:queue_manager) do
Gitlab::SidekiqMiddleware::ConcurrencyLimit::QueueManager.new(worker_name: resumed_worker_class.name,
prefix: 'sidekiq:concurrency_limit')
end
before do
2.times { queue_manager.add_to_queue!(['foo'], context) }
end
it 'enqueued jobs containing the correct payload' do
TestNormalWorker.perform_async
queue_manager.resume_processing!(limit: 2)
TestNormalWorker.perform_async
rw_jobs = TestResumedWorker.jobs
expect(rw_jobs.length).to be(2)
rw_jobs.each do |job|
expect(job['correlation_id']).to eq('context_correlation_id')
expect(job["#{Gitlab::ApplicationContext::LOG_KEY}.project"]).to eq('gitlab-org/gitlab')
expect(job['concurrency_limit_resume']).to be(true)
expect(job['concurrency_limit_buffered_at']).not_to be_nil
end
nw_jobs = TestNormalWorker.jobs
expect(nw_jobs.length).to be(2)
nw_jobs.each do |job|
expect(job).not_to include(
"#{Gitlab::ApplicationContext::LOG_KEY}.project",
'concurrency_limit_resume',
'concurrency_limit_buffered_at'
)
expect(job['correlation_id']).not_to eq('context_correlation_id')
end
end
end
end

View File

@ -50,6 +50,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
it { expect(setting.user_projects_api_limit).to eq(300) }
it { expect(setting.user_starred_projects_api_limit).to eq(100) }
it { expect(setting.disable_password_authentication_for_users_with_sso_identities).to eq(false) }
it { expect(setting.resource_usage_limits).to eq({}) }
end
describe 'USERS_UNCONFIRMED_SECONDARY_EMAILS_DELETE_AFTER_DAYS' do
@ -92,6 +93,7 @@ RSpec.describe ApplicationSetting, feature_category: :shared, type: :model do
}
end
it { expect(described_class).to validate_jsonb_schema(['resource_usage_limits']) }
it { expect(described_class).to validate_jsonb_schema(['application_setting_rate_limits']) }
it { expect(described_class).to validate_jsonb_schema(['application_setting_package_registry']) }
it { expect(described_class).to validate_jsonb_schema(['application_setting_service_ping_settings']) }

View File

@ -1174,5 +1174,31 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting, featu
expect(json_response['security_txt_content']).to eq(content)
end
end
context 'with resource usage limits' do
let(:hash) do
{
'rules' => [
{
'name' => 'test',
'rules' => [{ 'interval' => 1, 'threshold' => 10, 'selector' => '*' }],
'resource_key' => 'key',
'metadata' => {},
'scopes' => ['worker_name']
}
]
}
end
it 'updates the settings' do
put(
api("/application/settings", admin),
params: { resource_usage_limits: Gitlab::Json.dump(hash) }
)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['resource_usage_limits']).to eq(hash)
end
end
end
end

View File

@ -6821,10 +6821,10 @@ emittery@^0.13.1:
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad"
integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==
emoji-regex@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.0.tgz#96559e19f82231b436403e059571241d627c42b8"
integrity sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==
emoji-regex@^10.3.0:
version "10.4.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.4.0.tgz#03553afea80b3975749cfcb36f776ca268e413d4"
integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
emoji-regex@^8.0.0:
version "8.0.0"