Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3205d8a555
commit
3d53332cde
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
8efef05be30f0260bdab64a800c91ee81ffb4578
|
||||
fd83dcb146ce6211929033fd73c063273f51d08d
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -589,6 +589,7 @@ export const setNewWorkItemCache = async (
|
|||
setWorkItemMetadata: true,
|
||||
createNote: true,
|
||||
adminWorkItemLink: true,
|
||||
markNoteAsInternal: true,
|
||||
__typename: 'WorkItemPermissions',
|
||||
},
|
||||
widgets,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ fragment WorkItem on WorkItem {
|
|||
setWorkItemMetadata
|
||||
createNote
|
||||
adminWorkItemLink
|
||||
markNoteAsInternal
|
||||
}
|
||||
widgets {
|
||||
...WorkItemWidgets
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -100,6 +100,7 @@ $crud-header-min-height: px-to-rem(49px);
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.gl-pagination ul,
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
635883b6c6eeaa25516e0312455293e4d03a6923165d278160e9e76793d093ce
|
||||
|
|
@ -0,0 +1 @@
|
|||
2e4df57675f9a89a8a53d2158aa7e99368deca4585409b784f6b23e12ef858c6
|
||||
|
|
@ -0,0 +1 @@
|
|||
f5e6944be491fcffd5f59f24eb95ca527df0cafbc84ef46af4e63a00670d0043
|
||||
|
|
@ -0,0 +1 @@
|
|||
8c7a6d45ee7385db261c5d55f7cbef5b186843da3d4e4acdcaea2a4b5dae2138
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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` |
|
||||
|
|
|
|||
|
|
@ -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_.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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' => [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 `%%`'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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)$"
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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:' });
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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']) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue