diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-03 12:09:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-03 12:09:20 +0300 |
commit | 3d8459c18b7a20d9142359bb9334b467e774eb36 (patch) | |
tree | 109d881c19a27cdae131bf5e82f1216401d9df2b | |
parent | e0a415ccb7a7e59c7a6c16841bdd1668d2ef0be5 (diff) |
Add latest changes from gitlab-org/gitlab@master
45 files changed, 924 insertions, 17 deletions
diff --git a/Gemfile.lock b/Gemfile.lock index 334b819fa1f..a274df8450d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,15 +13,15 @@ PATH json (~> 2.6.3) PATH - remote: gems/error_tracking_open_api + remote: gems/csv_builder specs: - error_tracking_open_api (1.0.0) - typhoeus (~> 1.0, >= 1.0.1) + csv_builder (0.1.0) PATH - remote: gems/csv_builder + remote: gems/error_tracking_open_api specs: - csv_builder (0.1.0) + error_tracking_open_api (1.0.0) + typhoeus (~> 1.0, >= 1.0.1) PATH remote: gems/gitlab-rspec diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue new file mode 100644 index 00000000000..f90633c6e03 --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue @@ -0,0 +1,136 @@ +<script> +import { GlAlert, GlLoadingIcon, GlSprintf, GlLink, GlCard } from '@gitlab/ui'; +import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue'; +import axios from '~/lib/utils/axios_utils'; +import { + FEEDBACK_ISSUE_URL, + I18N_LOADING_LABEL, + I18N_CARD_TITLE, + I18N_GENERIC_ERROR, + I18N_FEEDBACK_PARAGRAPH, +} from '../custom_email_constants'; + +export default { + components: { + BetaBadge, + GlAlert, + GlLoadingIcon, + GlSprintf, + GlLink, + GlCard, + }, + FEEDBACK_ISSUE_URL, + I18N_LOADING_LABEL, + I18N_CARD_TITLE, + I18N_FEEDBACK_PARAGRAPH, + props: { + incomingEmail: { + type: String, + required: true, + default: '', + }, + customEmailEndpoint: { + type: String, + required: true, + default: '', + }, + }, + data() { + return { + loading: true, + customEmail: null, + enabled: false, + verificationState: null, + verificationError: null, + smtpAddress: null, + errorMessage: null, + alertMessage: null, + }; + }, + mounted() { + this.getCustomEmailDetails(); + }, + methods: { + dismissAlert() { + this.alertMessage = null; + }, + getCustomEmailDetails() { + axios + .get(this.customEmailEndpoint) + .then(({ data }) => { + this.updateData(data); + }) + .catch(this.handleRequestError) + .finally(() => { + this.loading = false; + }); + }, + handleRequestError() { + this.alertMessage = I18N_GENERIC_ERROR; + }, + updateData(data) { + this.customEmail = data.custom_email; + this.enabled = data.custom_email_enabled; + this.verificationState = data.custom_email_verification_state; + this.verificationError = data.custom_email_verification_error; + this.smtpAddress = data.custom_email_smtp_address; + this.errorMessage = data.error_message; + }, + }, +}; +</script> + +<template> + <div class="row gl-mt-7"> + <div class="col-md-9"> + <gl-card> + <template #header> + <div class="gl-display-flex align-items-center justify-content-between"> + <h5 class="gl-my-0">{{ $options.I18N_CARD_TITLE }}</h5> + <beta-badge /> + </div> + </template> + + <template #default> + <template v-if="loading"> + <div class="gl-p-3 gl-text-center"> + <gl-loading-icon + :label="$options.I18N_LOADING_LABEL" + size="md" + color="dark" + variant="spinner" + :inline="false" + /> + {{ $options.I18N_LOADING_LABEL }} + </div> + </template> + + <gl-alert + v-if="alertMessage" + variant="warning" + class="gl-mt-n5 gl-mx-n5" + @dismiss="dismissAlert" + > + {{ alertMessage }} + </gl-alert> + </template> + + <template #footer> + <span> + <gl-sprintf :message="$options.I18N_FEEDBACK_PARAGRAPH"> + <template #link="{ content }"> + <gl-link + :href="$options.FEEDBACK_ISSUE_URL" + data-testid="feedback-link" + target="_blank" + class="gl-text-blue-600 font-size-inherit" + >{{ content }} + </gl-link> + </template> + </gl-sprintf> + </span> + </template> + </gl-card> + </div> + </div> +</template> diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue index ae28694f5d2..3af4ea8af65 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -4,8 +4,11 @@ import SafeHtml from '~/vue_shared/directives/safe_html'; import axios from '~/lib/utils/axios_utils'; import { helpPagePath } from '~/helpers/help_page_helper'; import { __, sprintf } from '~/locale'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import ServiceDeskSetting from './service_desk_setting.vue'; +const CustomEmail = () => import('./custom_email.vue'); + export default { serviceDeskEmailHelpPath: helpPagePath('/user/project/service_desk.html', { anchor: 'use-an-additional-service-desk-alias-email', @@ -15,10 +18,12 @@ export default { GlSprintf, GlLink, ServiceDeskSetting, + CustomEmail, }, directives: { SafeHtml, }, + mixins: [glFeatureFlagsMixin()], inject: { initialIsEnabled: { default: false, @@ -56,6 +61,9 @@ export default { publicProject: { default: false, }, + customEmailEndpoint: { + default: '', + }, }, data() { return { @@ -68,6 +76,11 @@ export default { updatedServiceDeskEmail: this.serviceDeskEmail, }; }, + computed: { + showCustomEmail() { + return this.glFeatures.serviceDeskCustomEmail && this.isEnabled && this.isIssueTrackerEnabled; + }, + }, methods: { onEnableToggled(isChecked) { this.isEnabled = isChecked; @@ -179,5 +192,10 @@ export default { @save="onSaveTemplate" @toggle="onEnableToggled" /> + <custom-email + v-if="showCustomEmail" + :incoming-email="incomingEmail" + :custom-email-endpoint="customEmailEndpoint" + /> </div> </template> diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js new file mode 100644 index 00000000000..9770a1f4df9 --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js @@ -0,0 +1,10 @@ +import { s__, __ } from '~/locale'; + +export const FEEDBACK_ISSUE_URL = 'https://gitlab.com/gitlab-org/gitlab/-/issues/416637'; + +export const I18N_LOADING_LABEL = __('Loading'); +export const I18N_CARD_TITLE = s__('ServiceDesk|Configure a custom email address'); +export const I18N_FEEDBACK_PARAGRAPH = s__( + 'ServiceDesk|Please share your feedback on this feature in the %{linkStart}feedback issue%{linkEnd}', +); +export const I18N_GENERIC_ERROR = __('An error occurred. Please try again.'); diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js index 0f4c747a7b6..dd9585734db 100644 --- a/app/assets/javascripts/projects/settings_service_desk/index.js +++ b/app/assets/javascripts/projects/settings_service_desk/index.js @@ -22,6 +22,7 @@ export default () => { selectedFileTemplateProjectId, templates, publicProject, + customEmailEndpoint, } = el.dataset; return new Vue({ @@ -39,6 +40,7 @@ export default () => { selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null, templates: JSON.parse(templates), publicProject: parseBoolean(publicProject), + customEmailEndpoint, }, render: (createElement) => createElement(ServiceDeskRoot), }); diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb index ad484b09d4d..9f83e955f4c 100644 --- a/app/graphql/types/commit_type.rb +++ b/app/graphql/types/commit_type.rb @@ -34,6 +34,9 @@ module Types field :authored_date, type: Types::TimeType, null: true, description: 'Timestamp of when the commit was authored.' + field :committed_date, type: Types::TimeType, null: true, + description: 'Timestamp of when the commit was committed.' + field :web_url, type: GraphQL::Types::String, null: false, description: 'Web URL of the commit.' @@ -55,6 +58,12 @@ module Types field :author_name, type: GraphQL::Types::String, null: true, description: 'Commit authors name.' + field :committer_email, type: GraphQL::Types::String, null: true, + description: "Email of the committer." + + field :committer_name, type: GraphQL::Types::String, null: true, + description: "Name of the committer." + # models/commit lazy loads the author by email field :author, type: Types::UserType, null: true, description: 'Author of the commit.' diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb index a13cb353c7b..3c592c0008f 100644 --- a/app/models/merge_request/metrics.rb +++ b/app/models/merge_request/metrics.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class MergeRequest::Metrics < ApplicationRecord - include IgnorableColumns include DatabaseEventTracking belongs_to :merge_request, inverse_of: :metrics @@ -17,8 +16,6 @@ class MergeRequest::Metrics < ApplicationRecord scope :with_valid_time_to_merge, -> { where(arel_table[:merged_at].gt(arel_table[:created_at])) } scope :by_target_project, ->(project) { where(target_project_id: project) } - ignore_column :id_convert_to_bigint, remove_with: '16.0', remove_after: '2023-05-22' - class << self def time_to_merge_expression Arel.sql('EXTRACT(epoch FROM SUM(AGE(merge_request_metrics.merged_at, merge_request_metrics.created_at)))') diff --git a/app/views/projects/_service_desk_settings.html.haml b/app/views/projects/_service_desk_settings.html.haml index 0a83efdb3b8..b6e54e7395a 100644 --- a/app/views/projects/_service_desk_settings.html.haml +++ b/app/views/projects/_service_desk_settings.html.haml @@ -19,6 +19,7 @@ outgoing_name: "#{@project.service_desk_setting&.outgoing_name}", project_key: "#{@project.service_desk_setting&.project_key}", templates: available_service_desk_templates_for(@project), - public_project: "#{@project.public?}" } } + public_project: "#{@project.public?}", + custom_email_endpoint: project_service_desk_custom_email_path(@project) } } - elsif show_callout?('promote_service_desk_dismissed') = render 'shared/promotions/promote_servicedesk' diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 8d2219a0ef5..d278986266c 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -469,8 +469,6 @@ - 5 - - process_commit - 3 -- - product_analytics_initialize_analytics - - 1 - - product_analytics_initialize_snowplow_product_analytics - 1 - - product_analytics_post_push diff --git a/db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml b/db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml new file mode 100644 index 00000000000..c2dda4a2923 --- /dev/null +++ b/db/docs/batched_background_migrations/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.yml @@ -0,0 +1,6 @@ +--- +migration_job_name: BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob +description: Refreshes ProjectStatistics to remove pipeline_artifacts_size from the total storage_size +feature_category: consumables_cost_management +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126053 +milestone: 16.3 diff --git a/db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb b/db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb new file mode 100644 index 00000000000..631ca9ec550 --- /dev/null +++ b/db/migrate/20230714084415_add_is_unique_to_project_authorizations.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddIsUniqueToProjectAuthorizations < Gitlab::Database::Migration[2.1] + enable_lock_retries! + + def change + add_column :project_authorizations, :is_unique, :boolean, null: true + end +end diff --git a/db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb b/db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb new file mode 100644 index 00000000000..4ef493092a0 --- /dev/null +++ b/db/migrate/20230726104022_add_name_to_google_cloud_logging_configuration.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class AddNameToGoogleCloudLoggingConfiguration < Gitlab::Database::Migration[2.1] + # rubocop:disable Migration/AddLimitToTextColumns + # text limit is added in a 20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb migration + def change + add_column :audit_events_google_cloud_logging_configurations, :name, :text + end + + # rubocop:enable Migration/AddLimitToTextColumns +end diff --git a/db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb b/db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb new file mode 100644 index 00000000000..4a13bfda89e --- /dev/null +++ b/db/migrate/20230726104547_add_text_limit_to_google_cloud_logging_configuration_name.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitToGoogleCloudLoggingConfigurationName < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_text_limit :audit_events_google_cloud_logging_configurations, :name, 72 + end + + def down + remove_text_limit :audit_events_google_cloud_logging_configurations, :name + end +end diff --git a/db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb b/db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb new file mode 100644 index 00000000000..258d4583aa1 --- /dev/null +++ b/db/migrate/20230726104616_add_index_to_google_cloud_logging_configuration.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddIndexToGoogleCloudLoggingConfiguration < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'uniq_google_cloud_logging_configuration_namespace_id_and_name' + + def up + add_concurrent_index :audit_events_google_cloud_logging_configurations, [:namespace_id, :name], unique: true, + name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :audit_events_google_cloud_logging_configurations, INDEX_NAME + end +end diff --git a/db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb b/db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb new file mode 100644 index 00000000000..b1bd58d679a --- /dev/null +++ b/db/migrate/20230731130351_remove_initialize_analytics_worker_job_instances.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class RemoveInitializeAnalyticsWorkerJobInstances < Gitlab::Database::Migration[2.1] + DEPRECATED_JOB_CLASSES = %w[InitializeAnalyticsWorker] + + disable_ddl_transaction! + + def up + sidekiq_remove_jobs(job_klasses: DEPRECATED_JOB_CLASSES) + end + + def down + # This migration removes any instances of deprecated workers and cannot be undone. + end +end diff --git a/db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb b/db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb new file mode 100644 index 00000000000..d4d51131dda --- /dev/null +++ b/db/post_migrate/20230714095946_schedule_unique_index_project_authorizations_on_unique_project_user.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class ScheduleUniqueIndexProjectAuthorizationsOnUniqueProjectUser < Gitlab::Database::Migration[2.1] + INDEX_NAME = 'index_unique_project_authorizations_on_unique_project_user' + + def up + prepare_async_index :project_authorizations, + %i[project_id user_id], + unique: true, + where: "is_unique", + name: INDEX_NAME + end + + def down + unprepare_async_index :project_authorizations, + %i[project_id user_id], + name: INDEX_NAME + end +end diff --git a/db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb b/db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb new file mode 100644 index 00000000000..17e1e9e81f8 --- /dev/null +++ b/db/post_migrate/20230718145613_add_temp_index_for_project_statistics_pipeline_artifacts_size_migration.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddTempIndexForProjectStatisticsPipelineArtifactsSizeMigration < Gitlab::Database::Migration[2.1] + INDEX_PROJECT_STATSISTICS_PIPELINE_ARTIFACTS_SIZE = 'tmp_index_project_statistics_pipeline_artifacts_size' + + disable_ddl_transaction! + + def up + # Temporary index is to be used to trigger a refresh of project_statistics with + # pipeline_artifacts_size != 0 + add_concurrent_index :project_statistics, [:project_id], + name: INDEX_PROJECT_STATSISTICS_PIPELINE_ARTIFACTS_SIZE, + where: "pipeline_artifacts_size != 0" + end + + def down + remove_concurrent_index_by_name :project_statistics, INDEX_PROJECT_STATSISTICS_PIPELINE_ARTIFACTS_SIZE + end +end diff --git a/db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb b/db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb new file mode 100644 index 00000000000..135d3596960 --- /dev/null +++ b/db/post_migrate/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSize < Gitlab::Database::Migration[2.1] + restrict_gitlab_migration gitlab_schema: :gitlab_main + + MIGRATION = "BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob" + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 500 + SUB_BATCH_SIZE = 100 + + def up + return unless Gitlab.dev_or_test_env? || Gitlab.org_or_com? + + queue_batched_background_migration( + MIGRATION, + :project_statistics, + :project_id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + return unless Gitlab.dev_or_test_env? || Gitlab.org_or_com? + + delete_batched_background_migration(MIGRATION, :project_statistics, :project_id, []) + end +end diff --git a/db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb b/db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb new file mode 100644 index 00000000000..5e06170a506 --- /dev/null +++ b/db/post_migrate/20230731090319_add_notes_namespace_id_foreign_key.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddNotesNamespaceIdForeignKey < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'index_notes_on_namespace_id' + + def up + add_concurrent_index :notes, :namespace_id, name: INDEX_NAME + add_concurrent_foreign_key :notes, :namespaces, + column: :namespace_id, + on_delete: :cascade + end + + def down + remove_foreign_key_if_exists :notes, column: :namespace_id + remove_concurrent_index_by_name :notes, INDEX_NAME + end +end diff --git a/db/schema_migrations/20230714084415 b/db/schema_migrations/20230714084415 new file mode 100644 index 00000000000..4cfc303bbc1 --- /dev/null +++ b/db/schema_migrations/20230714084415 @@ -0,0 +1 @@ +0feaa606eef20192e84dfac7bc9285ca7c7baa56892fba99867c43b6b486dc33
\ No newline at end of file diff --git a/db/schema_migrations/20230714095946 b/db/schema_migrations/20230714095946 new file mode 100644 index 00000000000..26efcf8bed1 --- /dev/null +++ b/db/schema_migrations/20230714095946 @@ -0,0 +1 @@ +4cd83a7c498b698c7fa2b7adade3ac96005ff3408f92138416efa9c3a13ab05b
\ No newline at end of file diff --git a/db/schema_migrations/20230718145613 b/db/schema_migrations/20230718145613 new file mode 100644 index 00000000000..9202326eeed --- /dev/null +++ b/db/schema_migrations/20230718145613 @@ -0,0 +1 @@ +dc5291ab650b378e4bad5a395715a03535e2b217507ab36281dbf43b907fd9e1
\ No newline at end of file diff --git a/db/schema_migrations/20230719083202 b/db/schema_migrations/20230719083202 new file mode 100644 index 00000000000..03f8d98fa23 --- /dev/null +++ b/db/schema_migrations/20230719083202 @@ -0,0 +1 @@ +a604208ecfe76fe6ba380b804d81018e00a084146c4b29418ce4d447cb030c86
\ No newline at end of file diff --git a/db/schema_migrations/20230726104022 b/db/schema_migrations/20230726104022 new file mode 100644 index 00000000000..ca48184b4a5 --- /dev/null +++ b/db/schema_migrations/20230726104022 @@ -0,0 +1 @@ +d11b25fc925acdd86fe8ba25347a45dac314cf1963dbdaa6da58f63ade92cd2c
\ No newline at end of file diff --git a/db/schema_migrations/20230726104547 b/db/schema_migrations/20230726104547 new file mode 100644 index 00000000000..7657a19078b --- /dev/null +++ b/db/schema_migrations/20230726104547 @@ -0,0 +1 @@ +d880367d09bb1e563ddc61727b9bf852782853b78b822039bcf59fde4a714cc3
\ No newline at end of file diff --git a/db/schema_migrations/20230726104616 b/db/schema_migrations/20230726104616 new file mode 100644 index 00000000000..dde68062a7c --- /dev/null +++ b/db/schema_migrations/20230726104616 @@ -0,0 +1 @@ +15bfb68b92824b905681fedae219caa92ddd8976d6178497a76e2714db872a08
\ No newline at end of file diff --git a/db/schema_migrations/20230731090319 b/db/schema_migrations/20230731090319 new file mode 100644 index 00000000000..6849ff0f158 --- /dev/null +++ b/db/schema_migrations/20230731090319 @@ -0,0 +1 @@ +d3ec607d6efbc8f890648bad5ccaf84360ce0ba2417c0dfcb9b91eca08767cf4
\ No newline at end of file diff --git a/db/schema_migrations/20230731130351 b/db/schema_migrations/20230731130351 new file mode 100644 index 00000000000..b35240ee857 --- /dev/null +++ b/db/schema_migrations/20230731130351 @@ -0,0 +1 @@ +a68e6320db6c7d370aa72ccd0a262f989d9a2c1dd3b4c49fdae9dcb80fa04f59
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f88bb440002..2e66ac6efa3 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12240,9 +12240,11 @@ CREATE TABLE audit_events_google_cloud_logging_configurations ( log_id_name text DEFAULT 'audit_events'::text, encrypted_private_key bytea NOT NULL, encrypted_private_key_iv bytea NOT NULL, + name text, CONSTRAINT check_0ef835c61e CHECK ((char_length(client_email) <= 254)), CONSTRAINT check_55783c7c19 CHECK ((char_length(google_project_id_name) <= 30)), - CONSTRAINT check_898a76b005 CHECK ((char_length(log_id_name) <= 511)) + CONSTRAINT check_898a76b005 CHECK ((char_length(log_id_name) <= 511)), + CONSTRAINT check_cdf6883cd6 CHECK ((char_length(name) <= 72)) ); CREATE SEQUENCE audit_events_google_cloud_logging_configurations_id_seq @@ -20983,7 +20985,8 @@ ALTER SEQUENCE project_aliases_id_seq OWNED BY project_aliases.id; CREATE TABLE project_authorizations ( user_id integer NOT NULL, project_id integer NOT NULL, - access_level integer NOT NULL + access_level integer NOT NULL, + is_unique boolean ); CREATE TABLE project_auto_devops ( @@ -32383,6 +32386,8 @@ CREATE INDEX index_notes_on_id_where_internal ON notes USING btree (id) WHERE (i CREATE INDEX index_notes_on_line_code ON notes USING btree (line_code); +CREATE INDEX index_notes_on_namespace_id ON notes USING btree (namespace_id); + CREATE INDEX index_notes_on_noteable_id_and_noteable_type_and_system ON notes USING btree (noteable_id, noteable_type, system); CREATE INDEX index_notes_on_project_id_and_id_and_system_false ON notes USING btree (project_id, id) WHERE (NOT system); @@ -34007,12 +34012,16 @@ CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING CREATE INDEX tmp_index_project_statistics_cont_registry_size ON project_statistics USING btree (project_id) WHERE (container_registry_size = 0); +CREATE INDEX tmp_index_project_statistics_pipeline_artifacts_size ON project_statistics USING btree (project_id) WHERE (pipeline_artifacts_size <> 0); + CREATE INDEX tmp_index_vulnerability_dismissal_info ON vulnerabilities USING btree (id) WHERE ((state = 2) AND ((dismissed_at IS NULL) OR (dismissed_by_id IS NULL))); CREATE INDEX tmp_index_vulnerability_overlong_title_html ON vulnerabilities USING btree (id) WHERE (length(title_html) > 800); CREATE UNIQUE INDEX u_project_compliance_standards_adherence_for_reporting ON project_compliance_standards_adherence USING btree (project_id, check_name, standard); +CREATE UNIQUE INDEX uniq_google_cloud_logging_configuration_namespace_id_and_name ON audit_events_google_cloud_logging_configurations USING btree (namespace_id, name); + CREATE UNIQUE INDEX uniq_idx_packages_packages_on_project_id_name_version_ml_model ON packages_packages USING btree (project_id, name, version) WHERE (package_type = 14); CREATE UNIQUE INDEX uniq_idx_user_add_on_assignments_on_add_on_purchase_and_user ON subscription_user_add_on_assignments USING btree (add_on_purchase_id, user_id); @@ -36230,6 +36239,9 @@ ALTER TABLE ONLY environments ALTER TABLE ONLY vulnerabilities ADD CONSTRAINT fk_76bc5f5455 FOREIGN KEY (resolved_by_id) REFERENCES users(id) ON DELETE SET NULL; +ALTER TABLE ONLY notes + ADD CONSTRAINT fk_76db6d50c6 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY oauth_openid_requests ADD CONSTRAINT fk_77114b3b09 FOREIGN KEY (access_grant_id) REFERENCES oauth_access_grants(id) ON DELETE CASCADE; diff --git a/doc/administration/external_users.md b/doc/administration/external_users.md index f8ca379d10c..9be49fab95f 100644 --- a/doc/administration/external_users.md +++ b/doc/administration/external_users.md @@ -34,7 +34,7 @@ always take into account the as well as the permission level of the user. NOTE: -External users still count towards a license seat. +External users still count towards a license seat, unless the user has the [Guest role](../subscriptions/self_managed/index.md#free-guest-users) in the Ultimate tier. An administrator can flag a user as external by either of the following methods: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index ae0538104cf..5b4d8fc1389 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -13954,6 +13954,9 @@ Code Quality report for a pipeline. | <a id="commitauthorgravatar"></a>`authorGravatar` | [`String`](#string) | Commit authors gravatar. | | <a id="commitauthorname"></a>`authorName` | [`String`](#string) | Commit authors name. | | <a id="commitauthoreddate"></a>`authoredDate` | [`Time`](#time) | Timestamp of when the commit was authored. | +| <a id="commitcommitteddate"></a>`committedDate` | [`Time`](#time) | Timestamp of when the commit was committed. | +| <a id="commitcommitteremail"></a>`committerEmail` | [`String`](#string) | Email of the committer. | +| <a id="commitcommittername"></a>`committerName` | [`String`](#string) | Name of the committer. | | <a id="commitdescription"></a>`description` | [`String`](#string) | Description of the commit message. | | <a id="commitdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. | | <a id="commitdiffs"></a>`diffs` | [`[Diff!]`](#diff) | Diffs contained within the commit. This field can only be resolved for 10 diffs in any single request. | diff --git a/doc/user/permissions.md b/doc/user/permissions.md index ddb87f1e569..6e7dcb7e5fd 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -524,7 +524,7 @@ You can see the required minimal access levels and abilities requirements in the To associate a custom role with an existing group member, a group member with the Owner role: -1. Invites a user to the root group or any subgroup or project in the root +1. Invites a user as a direct member to the root group or any subgroup or project in the root group's hierarchy as a Guest. At this point, this Guest user cannot see any code on the projects in the group or subgroup. 1. Optional. If the Owner does not know the `ID` of the Guest user receiving a custom diff --git a/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb b/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb new file mode 100644 index 00000000000..88d0f27282a --- /dev/null +++ b/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # rubocop:disable Style/Documentation + class BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob < Gitlab::BackgroundMigration::BatchedMigrationJob # rubocop:disable Layout/LineLength + class Project < ::ApplicationRecord + self.table_name = 'projects' + + has_one :statistics, class_name: '::Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob::ProjectStatistics' # rubocop:disable Layout/LineLength + end + + class ProjectStatistics < ::ApplicationRecord + include ::EachBatch + + self.table_name = 'project_statistics' + + belongs_to :project, class_name: '::Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob::Project' # rubocop:disable Layout/LineLength + + def update_storage_size(storage_size_components) + new_storage_size = storage_size_components.sum { |component| method(component).call } + + # Only update storage_size if storage_size needs updating + return unless storage_size != new_storage_size + + self.storage_size = new_storage_size + save! + + ::Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) + log_with_data('Scheduled Namespaces::ScheduleAggregationWorker') + end + + def wiki_size + super.to_i + end + + def snippets_size + super.to_i + end + + private + + def log_with_data(log_line) + log_info( + log_line, + project_id: project.id, + pipeline_artifacts_size: pipeline_artifacts_size, + storage_size: storage_size, + namespace_id: project.namespace_id + ) + end + + def log_info(message, **extra) + ::Gitlab::BackgroundMigration::Logger.info( + migrator: 'BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob', + message: message, + **extra + ) + end + end + + scope_to ->(relation) { + relation.where.not(pipeline_artifacts_size: 0) + } + operation_name :update_storage_size + feature_category :consumables_cost_management + + def perform + each_sub_batch do |sub_batch| + ProjectStatistics.merge(sub_batch).each do |statistics| + statistics.update_storage_size(storage_size_components) + end + end + end + + private + + # Overridden in EE + def storage_size_components + [ + :repository_size, + :wiki_size, + :lfs_objects_size, + :build_artifacts_size, + :packages_size, + :snippets_size, + :uploads_size + ] + end + end + # rubocop:enable Style/Documentation + end +end + +Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob.prepend_mod diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 9bf0700b3b1..565e5c22561 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -43081,6 +43081,9 @@ msgstr "" msgid "ServiceDesk|Cannot update custom email" msgstr "" +msgid "ServiceDesk|Configure a custom email address" +msgstr "" + msgid "ServiceDesk|Custom email address could not be verified." msgstr "" @@ -43108,6 +43111,9 @@ msgstr "" msgid "ServiceDesk|Parameters missing" msgstr "" +msgid "ServiceDesk|Please share your feedback on this feature in the %{linkStart}feedback issue%{linkEnd}" +msgstr "" + msgid "ServiceDesk|Service Desk is not enabled" msgstr "" diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 882924c003b..8092b69d377 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -77,6 +77,11 @@ module QA has_element?('delete-issue-button') end + def has_no_delete_issue_button? + open_actions_dropdown + has_no_element?('delete-issue-button') + end + def delete_issue has_delete_issue_button? diff --git a/qa/qa/support/matchers/have_matcher.rb b/qa/qa/support/matchers/have_matcher.rb index c77b585ada8..6fa88ef205b 100644 --- a/qa/qa/support/matchers/have_matcher.rb +++ b/qa/qa/support/matchers/have_matcher.rb @@ -30,6 +30,7 @@ module QA alert_with_title incident framework + delete_issue_button ].each do |predicate| RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs| match do |page_object| diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js new file mode 100644 index 00000000000..f167d2e9d6e --- /dev/null +++ b/spec/frontend/projects/settings_service_desk/components/custom_email_spec.js @@ -0,0 +1,94 @@ +import { nextTick } from 'vue'; +import { GlLoadingIcon, GlAlert } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { HTTP_STATUS_OK, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status'; +import CustomEmail from '~/projects/settings_service_desk/components/custom_email.vue'; +import { + FEEDBACK_ISSUE_URL, + I18N_GENERIC_ERROR, +} from '~/projects/settings_service_desk/custom_email_constants'; +import { MOCK_CUSTOM_EMAIL_EMPTY } from './mock_data'; + +describe('CustomEmail', () => { + let axiosMock; + let wrapper; + + const defaultProps = { + incomingEmail: 'incoming@example.com', + customEmailEndpoint: '/flightjs/Flight/-/service_desk/custom_email', + }; + + const createWrapper = (props = {}) => { + wrapper = extendedWrapper( + mount(CustomEmail, { + propsData: { ...defaultProps, ...props }, + }), + ); + }; + + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAlert = () => wrapper.findComponent(GlAlert); + const findFeedbackLink = () => wrapper.findByTestId('feedback-link'); + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + it('displays link to feedback issue', () => { + createWrapper(); + + expect(findFeedbackLink().attributes('href')).toEqual(FEEDBACK_ISSUE_URL); + }); + + describe('when initial resource loading returns no configured custom email', () => { + beforeEach(() => { + axiosMock + .onGet(defaultProps.customEmailEndpoint) + .reply(HTTP_STATUS_OK, MOCK_CUSTOM_EMAIL_EMPTY); + + createWrapper(); + }); + + it('displays loading icon while fetching data', async () => { + // while loading + expect(findLoadingIcon().exists()).toBe(true); + await waitForPromises(); + // loading completed + expect(findLoadingIcon().exists()).toBe(false); + }); + }); + + describe('when initial resource loading returns 404', () => { + beforeEach(async () => { + axiosMock.onGet(defaultProps.customEmailEndpoint).reply(HTTP_STATUS_NOT_FOUND); + + createWrapper(); + await waitForPromises(); + }); + + it('displays error alert with correct text', () => { + expect(findLoadingIcon().exists()).toBe(false); + + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(I18N_GENERIC_ERROR); + }); + + it('dismissing the alert removes it', async () => { + expect(findAlert().exists()).toBe(true); + + findAlert().vm.$emit('dismiss'); + + await nextTick(); + + expect(findAlert().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/projects/settings_service_desk/components/mock_data.js b/spec/frontend/projects/settings_service_desk/components/mock_data.js index 934778ff601..ea88a6cfccd 100644 --- a/spec/frontend/projects/settings_service_desk/components/mock_data.js +++ b/spec/frontend/projects/settings_service_desk/components/mock_data.js @@ -6,3 +6,12 @@ export const TEMPLATES = [ { name: 'Security release', project_id: 1 }, ], ]; + +export const MOCK_CUSTOM_EMAIL_EMPTY = { + custom_email: null, + custom_email_enabled: false, + custom_email_verification_state: null, + custom_email_verification_error: null, + custom_email_smtp_address: null, + error_message: null, +}; diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js index b84d1c9c0aa..60f0efd9195 100644 --- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js @@ -6,6 +6,7 @@ import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue'; import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue'; +import CustomEmail from '~/projects/settings_service_desk/components/custom_email.vue'; describe('ServiceDeskRoot', () => { let axiosMock; @@ -25,6 +26,10 @@ describe('ServiceDeskRoot', () => { selectedFileTemplateProjectId: 42, templates: ['Bug', 'Documentation'], publicProject: false, + customEmailEndpoint: '/gitlab-org/gitlab-test/-/service_desk/custom_email', + glFeatures: { + serviceDeskCustomEmail: true, + }, }; const getAlertText = () => wrapper.findComponent(GlAlert).text(); @@ -186,4 +191,46 @@ describe('ServiceDeskRoot', () => { }); }); }); + + describe('CustomEmail component', () => { + it('is rendered', () => { + wrapper = createComponent(); + + expect(wrapper.findComponent(CustomEmail).exists()).toBe(true); + expect(wrapper.findComponent(CustomEmail).props()).toEqual({ + incomingEmail: provideData.initialIncomingEmail, + customEmailEndpoint: provideData.customEmailEndpoint, + }); + }); + + describe('when Service Desk is disabled', () => { + beforeEach(() => { + wrapper = createComponent({ initialIsEnabled: false }); + }); + + it('is not rendered', () => { + expect(wrapper.findComponent(CustomEmail).exists()).toBe(false); + }); + }); + + describe('when issue tracker is disabled', () => { + beforeEach(() => { + wrapper = createComponent({ isIssueTrackerEnabled: false }); + }); + + it('is not rendered', () => { + expect(wrapper.findComponent(CustomEmail).exists()).toBe(false); + }); + }); + + describe('when feature flag service_desk_custom_email is disabled', () => { + beforeEach(() => { + wrapper = createComponent({ glFeatures: { serviceDeskCustomEmail: false } }); + }); + + it('is not rendered', () => { + expect(wrapper.findComponent(CustomEmail).exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb index 3912b0905e3..6af5ea04dd2 100644 --- a/spec/graphql/types/commit_type_spec.rb +++ b/spec/graphql/types/commit_type_spec.rb @@ -13,7 +13,7 @@ RSpec.describe GitlabSchema.types['Commit'] do expect(described_class).to have_graphql_fields( :id, :sha, :short_id, :title, :full_title, :full_title_html, :description, :description_html, :message, :title_html, :authored_date, :author_name, :author_email, :author_gravatar, :author, :diffs, :web_url, :web_path, - :pipelines, :signature_html, :signature + :pipelines, :signature_html, :signature, :committer_name, :committer_email, :committed_date ) end diff --git a/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb new file mode 100644 index 00000000000..c85636f4998 --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_project_statistics_storage_size_without_pipeline_artifacts_size_job_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSizeJob, + schema: 20230719083202, + feature_category: :consumables_cost_management do + include MigrationHelpers::ProjectStatisticsHelper + + include_context 'when backfilling project statistics' + + let(:default_pipeline_artifacts_size) { 5 } + let(:default_stats) do + { + repository_size: 1, + wiki_size: 1, + lfs_objects_size: 1, + build_artifacts_size: 1, + packages_size: 1, + snippets_size: 1, + uploads_size: 1, + pipeline_artifacts_size: default_pipeline_artifacts_size, + storage_size: default_storage_size + } + end + + describe '#filter_batch' do + it 'filters out project_statistics with no artifacts size' do + project_statistics = generate_records(default_projects, project_statistics_table, default_stats) + project_statistics_table.create!( + project_id: proj5.id, + namespace_id: proj5.namespace_id, + repository_size: 1, + wiki_size: 1, + lfs_objects_size: 1, + build_artifacts_size: 1, + packages_size: 1, + snippets_size: 1, + pipeline_artifacts_size: 0, + uploads_size: 1, + storage_size: 7 + ) + + expected = project_statistics.map(&:id) + actual = migration.filter_batch(project_statistics_table).pluck(:id) + + expect(actual).to match_array(expected) + end + end + + describe '#perform' do + subject(:perform_migration) { migration.perform } + + context 'when project_statistics backfill runs' do + before do + generate_records(default_projects, project_statistics_table, default_stats) + end + + context 'when storage_size includes pipeline_artifacts_size' do + it 'removes pipeline_artifacts_size from storage_size' do + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + expect(project_statistics_table.pluck(:storage_size).uniq).to match_array([default_storage_size]) + + perform_migration + + expect(project_statistics_table.pluck(:storage_size).uniq).to match_array( + [default_storage_size - default_pipeline_artifacts_size] + ) + expect(::Namespaces::ScheduleAggregationWorker).to have_received(:perform_async).exactly(4).times + end + end + + context 'when storage_size does not include default_pipeline_artifacts_size' do + it 'does not update the record' do + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + proj_stat = project_statistics_table.last + expect(proj_stat.storage_size).to eq(default_storage_size) + proj_stat.storage_size = default_storage_size - default_pipeline_artifacts_size + proj_stat.save! + + perform_migration + + expect(project_statistics_table.pluck(:storage_size).uniq).to match_array( + [default_storage_size - default_pipeline_artifacts_size] + ) + expect(::Namespaces::ScheduleAggregationWorker).to have_received(:perform_async).exactly(3).times + end + end + end + + it 'coerces a null wiki_size to 0' do + project_statistics = create_project_stats(projects, namespaces, default_stats, { wiki_size: nil }) + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + migration = create_migration(end_id: project_statistics.project_id) + + migration.perform + + project_statistics.reload + expect(project_statistics.storage_size).to eq(6) + end + + it 'coerces a null snippets_size to 0' do + project_statistics = create_project_stats(projects, namespaces, default_stats, { snippets_size: nil }) + allow(::Namespaces::ScheduleAggregationWorker).to receive(:perform_async) + migration = create_migration(end_id: project_statistics.project_id) + + migration.perform + + project_statistics.reload + expect(project_statistics.storage_size).to eq(6) + end + end +end diff --git a/spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb b/spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb new file mode 100644 index 00000000000..c3183a5da1b --- /dev/null +++ b/spec/migrations/20230719083202_backfill_project_statistics_storage_size_without_pipeline_artifacts_size_spec.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe BackfillProjectStatisticsStorageSizeWithoutPipelineArtifactsSize, feature_category: :consumables_cost_management do + let!(:batched_migration) { described_class::MIGRATION } + + it 'does not schedule background jobs when Gitlab.org_or_com? is false' do + allow(Gitlab).to receive(:dev_or_test_env?).and_return(false) + allow(Gitlab).to receive(:org_or_com?).and_return(false) + + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + end + end + + it 'schedules a new batched migration' do + allow(Gitlab).to receive(:dev_or_test_env?).and_return(false) + allow(Gitlab).to receive(:org_or_com?).and_return(true) + + reversible_migration do |migration| + migration.before -> { + expect(batched_migration).not_to have_scheduled_batched_migration + } + + migration.after -> { + expect(batched_migration).to have_scheduled_batched_migration( + table_name: :project_statistics, + column_name: :project_id, + interval: described_class::DELAY_INTERVAL, + batch_size: described_class::BATCH_SIZE, + sub_batch_size: described_class::SUB_BATCH_SIZE + ) + } + end + end +end diff --git a/spec/support/helpers/migrations_helpers/project_statistics_helper.rb b/spec/support/helpers/migrations_helpers/project_statistics_helper.rb new file mode 100644 index 00000000000..4e7d83a38ac --- /dev/null +++ b/spec/support/helpers/migrations_helpers/project_statistics_helper.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module MigrationHelpers + module ProjectStatisticsHelper + def generate_records(projects, table, values = {}) + projects.map do |proj| + table.create!( + values.merge({ + project_id: proj.id, + namespace_id: proj.namespace_id + }) + ) + end + end + + def create_migration(end_id:) + described_class.new(start_id: 1, end_id: end_id, + batch_table: 'project_statistics', batch_column: 'project_id', + sub_batch_size: 1_000, pause_ms: 0, + connection: ApplicationRecord.connection) + end + + def create_project_stats(project_table, namespace, default_stats, override_stats = {}) + stats = default_stats.merge(override_stats) + + group = namespace.create!(name: 'group_a', path: 'group-a', type: 'Group') + project_namespace = namespace.create!(name: 'project_a', path: 'project_a', type: 'Project', parent_id: group.id) + proj = project_table.create!(name: 'project_a', path: 'project-a', namespace_id: group.id, + project_namespace_id: project_namespace.id) + project_statistics_table.create!( + project_id: proj.id, + namespace_id: group.id, + **stats + ) + end + end +end diff --git a/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb b/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb new file mode 100644 index 00000000000..1b835e1392d --- /dev/null +++ b/spec/support/shared_contexts/lib/gitlab/background_migration/backfill_project_statistics.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +RSpec.shared_context 'when backfilling project statistics' do + let!(:namespaces) { table(:namespaces) } + let!(:project_statistics_table) { table(:project_statistics) } + let!(:projects) { table(:projects) } + let!(:count_of_columns) { ProjectStatistics::STORAGE_SIZE_COMPONENTS.count } + let(:default_storage_size) { 12 } + + let!(:root_group) do + namespaces.create!(name: 'root-group', path: 'root-group', type: 'Group') do |new_group| + new_group.update!(traversal_ids: [new_group.id]) + end + end + + let!(:group) do + namespaces.create!(name: 'group', path: 'group', parent_id: root_group.id, type: 'Group') do |new_group| + new_group.update!(traversal_ids: [root_group.id, new_group.id]) + end + end + + let!(:sub_group) do + namespaces.create!(name: 'subgroup', path: 'subgroup', parent_id: group.id, type: 'Group') do |new_group| + new_group.update!(traversal_ids: [root_group.id, group.id, new_group.id]) + end + end + + let!(:namespace1) do + namespaces.create!( + name: 'namespace1', type: 'Group', path: 'space1' + ) + end + + let!(:proj_namespace1) do + namespaces.create!( + name: 'proj1', path: 'proj1', type: 'Project', parent_id: namespace1.id + ) + end + + let!(:proj_namespace2) do + namespaces.create!( + name: 'proj2', path: 'proj2', type: 'Project', parent_id: namespace1.id + ) + end + + let!(:proj_namespace3) do + namespaces.create!( + name: 'proj3', path: 'proj3', type: 'Project', parent_id: sub_group.id + ) + end + + let!(:proj_namespace4) do + namespaces.create!( + name: 'proj4', path: 'proj4', type: 'Project', parent_id: sub_group.id + ) + end + + let!(:proj_namespace5) do + namespaces.create!( + name: 'proj5', path: 'proj5', type: 'Project', parent_id: sub_group.id + ) + end + + let!(:proj1) do + projects.create!( + name: 'proj1', path: 'proj1', namespace_id: namespace1.id, project_namespace_id: proj_namespace1.id + ) + end + + let!(:proj2) do + projects.create!( + name: 'proj2', path: 'proj2', namespace_id: namespace1.id, project_namespace_id: proj_namespace2.id + ) + end + + let!(:proj3) do + projects.create!( + name: 'proj3', path: 'proj3', namespace_id: sub_group.id, project_namespace_id: proj_namespace3.id + ) + end + + let!(:proj4) do + projects.create!( + name: 'proj4', path: 'proj4', namespace_id: sub_group.id, project_namespace_id: proj_namespace4.id + ) + end + + let!(:proj5) do + projects.create!( + name: 'proj5', path: 'proj5', namespace_id: sub_group.id, project_namespace_id: proj_namespace5.id + ) + end + + let(:migration) do + described_class.new(start_id: 1, end_id: proj4.id, + batch_table: 'project_statistics', batch_column: 'project_id', + sub_batch_size: 1_000, pause_ms: 0, + connection: ApplicationRecord.connection) + end + + let(:default_projects) do + [ + proj1, proj2, proj3, proj4 + ] + end +end diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb index 38959b6d764..3cd030e678d 100644 --- a/spec/workers/every_sidekiq_worker_spec.rb +++ b/spec/workers/every_sidekiq_worker_spec.rb @@ -398,7 +398,6 @@ RSpec.describe 'Every Sidekiq worker', feature_category: :shared do 'PipelineProcessWorker' => 3, 'PostReceive' => 3, 'ProcessCommitWorker' => 3, - 'ProductAnalytics::InitializeAnalyticsWorker' => 3, 'ProductAnalytics::InitializeSnowplowProductAnalyticsWorker' => 1, 'ProjectCacheWorker' => 3, 'ProjectDestroyWorker' => 3, |