From 75621c94b5dbe233edd72c3d8cc602fed25e84d2 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 17 May 2023 06:07:11 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .gitlab/merge_request_templates/Default.md | 4 + .rubocop.yml | 5 + .../graphql/resource_not_available_error.yml | 42 +++++++++ app/assets/javascripts/layout_nav.js | 15 +++ app/assets/stylesheets/framework/mixins.scss | 10 +- app/controllers/sent_notifications_controller.rb | 4 + app/models/issue.rb | 6 ++ app/services/ci/job_artifacts/create_service.rb | 3 +- app/uploaders/object_storage.rb | 28 ++++-- app/views/admin/jobs/index.html.haml | 6 +- app/views/admin/projects/index.html.haml | 6 +- app/views/admin/users/_users.html.haml | 4 +- app/views/dashboard/_projects_head.html.haml | 6 +- .../merge_requests/creations/_new_submit.html.haml | 12 ++- app/views/shared/_event_filter.html.haml | 6 +- app/views/shared/milestones/_tabs.html.haml | 6 +- app/views/users/show.html.haml | 6 +- ...ue_notes_id_convert_to_bigint_for_gitlab_com.rb | 34 +++++++ ...ferencing_bigint_fks_for_notes_on_gitlab_com.rb | 74 +++++++++++++++ db/schema_migrations/20230516032545 | 1 + db/schema_migrations/20230516033729 | 1 + db/structure.sql | 50 ++++++++++ doc/ci/cloud_services/google_cloud/index.md | 16 +++- .../application_security/coverage_fuzzing/index.md | 4 +- locale/gitlab.pot | 68 ++++++++------ .../cop/graphql/resource_not_available_error.rb | 47 ++++++++++ rubocop/node_pattern_helper.rb | 14 +++ .../sent_notifications_controller_spec.rb | 56 +++++++++-- spec/frontend/layout_nav_spec.js | 39 ++++++++ spec/models/issue_spec.rb | 24 +++++ .../graphql/resource_not_available_error_spec.rb | 37 ++++++++ spec/rubocop/node_pattern_helper_spec.rb | 20 ++++ .../ci/job_artifacts/create_service_spec.rb | 6 +- spec/uploaders/object_storage_spec.rb | 104 ++++++++++++--------- 34 files changed, 652 insertions(+), 112 deletions(-) create mode 100644 .rubocop_todo/graphql/resource_not_available_error.yml create mode 100644 db/post_migrate/20230516032545_add_unique_notes_id_convert_to_bigint_for_gitlab_com.rb create mode 100644 db/post_migrate/20230516033729_add_referencing_bigint_fks_for_notes_on_gitlab_com.rb create mode 100644 db/schema_migrations/20230516032545 create mode 100644 db/schema_migrations/20230516033729 create mode 100644 rubocop/cop/graphql/resource_not_available_error.rb create mode 100644 rubocop/node_pattern_helper.rb create mode 100644 spec/frontend/layout_nav_spec.js create mode 100644 spec/rubocop/cop/graphql/resource_not_available_error_spec.rb create mode 100644 spec/rubocop/node_pattern_helper_spec.rb diff --git a/.gitlab/merge_request_templates/Default.md b/.gitlab/merge_request_templates/Default.md index 404a18ad074..254e90d5fb0 100644 --- a/.gitlab/merge_request_templates/Default.md +++ b/.gitlab/merge_request_templates/Default.md @@ -20,6 +20,10 @@ reviewers and future readers. If you need help visually verifying the change, please leave a comment and ping a GitLab reviewer, maintainer, or MR coach. --> +| Before | After | +| ------ | ------ | +| | | + ## How to set up and validate locally _Numbered steps to set up and validate the change are strongly suggested._ diff --git a/.rubocop.yml b/.rubocop.yml index 81ad4cd31f3..9ceb8466270 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1007,3 +1007,8 @@ SidekiqLoadBalancing/WorkerDataConsistency: # This cop is disabled for Ruby 3.0+ anyway. Lint/NonDeterministicRequireOrder: Enabled: false + +Graphql/ResourceNotAvailableError: + Exclude: + # Definition of `raise_resource_not_available_error!` + - 'lib/gitlab/graphql/authorize/authorize_resource.rb' diff --git a/.rubocop_todo/graphql/resource_not_available_error.yml b/.rubocop_todo/graphql/resource_not_available_error.yml new file mode 100644 index 00000000000..68e68c51277 --- /dev/null +++ b/.rubocop_todo/graphql/resource_not_available_error.yml @@ -0,0 +1,42 @@ +--- +# Cop supports --autocorrect. +Graphql/ResourceNotAvailableError: + Details: grace period + Exclude: + - 'app/graphql/mutations/achievements/create.rb' + - 'app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb' + - 'app/graphql/mutations/ci/ci_cd_settings_update.rb' + - 'app/graphql/mutations/ci/job_artifact/bulk_destroy.rb' + - 'app/graphql/mutations/ci/runner/create.rb' + - 'app/graphql/mutations/custom_emoji/create.rb' + - 'app/graphql/mutations/custom_emoji/destroy.rb' + - 'app/graphql/mutations/design_management/move.rb' + - 'app/graphql/mutations/issues/bulk_update.rb' + - 'app/graphql/mutations/issues/set_crm_contacts.rb' + - 'app/graphql/mutations/issues/set_escalation_status.rb' + - 'app/graphql/mutations/notes/create/base.rb' + - 'app/graphql/mutations/notes/create/note.rb' + - 'app/graphql/mutations/notes/reposition_image_diff_note.rb' + - 'app/graphql/mutations/notes/update/image_diff_note.rb' + - 'app/graphql/mutations/saved_replies/create.rb' + - 'app/graphql/mutations/saved_replies/destroy.rb' + - 'app/graphql/mutations/saved_replies/update.rb' + - 'app/graphql/mutations/todos/mark_all_done.rb' + - 'app/graphql/mutations/work_items/export.rb' + - 'app/graphql/resolvers/ci/runner_setup_resolver.rb' + - 'app/graphql/resolvers/concerns/search_arguments.rb' + - 'app/graphql/resolvers/container_repository_tags_resolver.rb' + - 'app/graphql/resolvers/design_management/versions_resolver.rb' + - 'app/graphql/resolvers/kas/agent_configurations_resolver.rb' + - 'app/graphql/resolvers/kas/agent_connections_resolver.rb' + - 'app/graphql/resolvers/projects/snippets_resolver.rb' + - 'app/graphql/types/container_repository_details_type.rb' + - 'app/graphql/types/container_repository_type.rb' + - 'ee/app/graphql/ee/types/query_type.rb' + - 'ee/app/graphql/mutations/ai/action.rb' + - 'ee/app/graphql/mutations/audit_events/instance_external_audit_event_destinations/base.rb' + - 'ee/app/graphql/mutations/issues/set_escalation_policy.rb' + - 'ee/app/graphql/mutations/projects/set_locked.rb' + - 'ee/app/graphql/resolvers/incident_management/oncall_shifts_resolver.rb' + - 'ee/app/graphql/resolvers/product_analytics/visualization_resolver.rb' + - 'ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb' diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js index f5078962b8f..39eb1d934ce 100644 --- a/app/assets/javascripts/layout_nav.js +++ b/app/assets/javascripts/layout_nav.js @@ -16,6 +16,21 @@ export function initScrollingTabs() { const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized'); $scrollingTabs.addClass('is-initialized'); + const el = $scrollingTabs.get(0); + const parentElement = el?.parentNode; + if (el && parentElement) { + parentElement + .querySelector('button.fade-left') + .addEventListener('click', function scrollLeft() { + el.scrollBy({ left: -200, behavior: 'smooth' }); + }); + parentElement + .querySelector('button.fade-right') + .addEventListener('click', function scrollRight() { + el.scrollBy({ left: 200, behavior: 'smooth' }); + }); + } + $(window) .on('resize.nav', () => { hideEndFade($scrollingTabs); diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 15a31fbb3d9..dd1989aa5e1 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -156,6 +156,12 @@ background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4)); + border: 0; + padding: 0; + + &:hover { + @include gl-focus; + } &.scrolling { visibility: visible; @@ -164,8 +170,8 @@ } svg { - position: relative; - top: 5px; + position: absolute; + top: 12px; font-size: 18px; } } diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb index 6069924b39a..6c5e709a98a 100644 --- a/app/controllers/sent_notifications_controller.rb +++ b/app/controllers/sent_notifications_controller.rb @@ -29,6 +29,10 @@ class SentNotificationsController < ApplicationController def unsubscribe_and_redirect noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project) + if noteable.is_a?(Issue) && @sent_notification.recipient_id == User.support_bot.id + noteable.unsubscribe_email_participant(noteable.external_author) + end + flash[:notice] = _("You have been unsubscribed from this thread.") if current_user diff --git a/app/models/issue.rb b/app/models/issue.rb index b7125617034..51deb8dea68 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -758,6 +758,12 @@ class Issue < ApplicationRecord end end + def unsubscribe_email_participant(email) + return if email.blank? + + issue_email_participants.find_by_email(email)&.destroy + end + private def check_issue_type_in_sync! diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb index f7e04c59463..1647e921092 100644 --- a/app/services/ci/job_artifacts/create_service.rb +++ b/app/services/ci/job_artifacts/create_service.rb @@ -26,7 +26,8 @@ module Ci headers = JobArtifactUploader.workhorse_authorize( has_length: false, maximum_size: max_size(artifact_type), - use_final_store_path: Feature.enabled?(:ci_artifacts_upload_to_final_location, project) + use_final_store_path: Feature.enabled?(:ci_artifacts_upload_to_final_location, project), + final_store_path_root_id: project.id ) if lsif?(artifact_type) diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb index 4188e0caa8e..e1e103e20f8 100644 --- a/app/uploaders/object_storage.rb +++ b/app/uploaders/object_storage.rb @@ -11,6 +11,7 @@ module ObjectStorage RemoteStoreError = Class.new(StandardError) UnknownStoreError = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError) + MissingFinalStorePathRootId = Class.new(StandardError) class ExclusiveLeaseTaken < StandardError def initialize(lease_key) @@ -153,21 +154,30 @@ module ObjectStorage [CarrierWave.generate_cache_id, SecureRandom.hex].join('-') end - def generate_final_store_path + def generate_final_store_path(root_id:) hash = Digest::SHA2.hexdigest(SecureRandom.uuid) # We prefix '@final' to prevent clashes and make the files easily recognizable # as having been created by this code. - File.join('@final', hash[0..1], hash[2..3], hash[4..]) + sub_path = File.join('@final', hash[0..1], hash[2..3], hash[4..]) + + # We generate a hashed path of the root ID (e.g. Project ID) to distribute directories instead of + # filling up one root directory with a bunch of files. + Gitlab::HashedPath.new(sub_path, root_hash: root_id).to_s end - def workhorse_authorize(has_length:, maximum_size: nil, use_final_store_path: false) + def workhorse_authorize( + has_length:, + maximum_size: nil, + use_final_store_path: false, + final_store_path_root_id: nil) {}.tap do |hash| if self.direct_upload_to_object_store? hash[:RemoteObject] = workhorse_remote_upload_options( has_length: has_length, maximum_size: maximum_size, - use_final_store_path: use_final_store_path + use_final_store_path: use_final_store_path, + final_store_path_root_id: final_store_path_root_id ) else hash[:TempPath] = workhorse_local_upload_path @@ -190,11 +200,17 @@ module ObjectStorage ObjectStorage::Config.new(object_store_options) end - def workhorse_remote_upload_options(has_length:, maximum_size: nil, use_final_store_path: false) + def workhorse_remote_upload_options( + has_length:, + maximum_size: nil, + use_final_store_path: false, + final_store_path_root_id: nil) return unless direct_upload_to_object_store? if use_final_store_path - id = generate_final_store_path + raise MissingFinalStorePathRootId unless final_store_path_root_id.present? + + id = generate_final_store_path(root_id: final_store_path_root_id) upload_path = with_bucket_prefix(id) prepare_pending_direct_upload(id) else diff --git a/app/views/admin/jobs/index.html.haml b/app/views/admin/jobs/index.html.haml index 7b00019cc21..c5632e0d70b 100644 --- a/app/views/admin/jobs/index.html.haml +++ b/app/views/admin/jobs/index.html.haml @@ -10,8 +10,10 @@ - else .top-area .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) } = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index e942a513166..31ec4935f64 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -4,8 +4,10 @@ .top-area.gl-flex-direction-column-reverse .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) = gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav nav gl-tabs-nav' }) do = gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? } = gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index c9264535a13..213d5847986 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -9,9 +9,9 @@ .top-area .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full - .fade-left + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } = sprite_icon('chevron-lg-left', size: 12) - .fade-right + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } = sprite_icon('chevron-lg-right', size: 12) = gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full' }) do = gl_tab_link_to admin_users_path, { item_active: active_when(params[:filter].nil?), class: 'gl-border-0!' } do diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index e600d84f492..b72b252a852 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -12,8 +12,10 @@ .top-area .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-flex-basis-0.gl-min-w-0 - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) = render 'dashboard/projects_nav' .nav-controls = render 'shared/projects/search_form' diff --git a/app/views/projects/merge_requests/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index 35e8b30e6e9..bec7cb3fd34 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -15,8 +15,10 @@ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } .merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix %li.commits-tab.new-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do @@ -32,8 +34,10 @@ .merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') } .merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) %ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix %li.commits-tab.new-tab = link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 03534bf78d1..82b4a314b59 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -1,8 +1,10 @@ - show_group_events = local_assigns.fetch(:show_group_events, false) .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.flex-fill - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) %ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs = event_filter_link EventFilter::ALL, _('All'), s_('EventFilterBy|Filter by all') - if event_filter_visible(:repository) diff --git a/app/views/shared/milestones/_tabs.html.haml b/app/views/shared/milestones/_tabs.html.haml index 8c49977fe82..cd1667cb3b3 100644 --- a/app/views/shared/milestones/_tabs.html.haml +++ b/app/views/shared/milestones/_tabs.html.haml @@ -1,8 +1,10 @@ - show_project_name = local_assigns.fetch(:show_project_name, false) .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) = gl_tabs_nav({ class: %w[scrolling-tabs js-milestone-tabs] }) do = gl_tab_link_to '#tab-issues', item_active: true, data: { endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do = _('Issues') diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 1ebf02ffd39..4113a276416 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -131,8 +131,10 @@ - if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user) .scrolling-tabs-container{ class: [('gl-display-none' if show_super_sidebar?)] } - .fade-left= sprite_icon('chevron-lg-left', size: 12) - .fade-right= sprite_icon('chevron-lg-right', size: 12) + %button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') } + = sprite_icon('chevron-lg-left', size: 12) + %button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') } + = sprite_icon('chevron-lg-right', size: 12) %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs.gl-border-b-0 - if profile_tab?(:overview) %li.js-overview-tab diff --git a/db/post_migrate/20230516032545_add_unique_notes_id_convert_to_bigint_for_gitlab_com.rb b/db/post_migrate/20230516032545_add_unique_notes_id_convert_to_bigint_for_gitlab_com.rb new file mode 100644 index 00000000000..330bf23434d --- /dev/null +++ b/db/post_migrate/20230516032545_add_unique_notes_id_convert_to_bigint_for_gitlab_com.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +class AddUniqueNotesIdConvertToBigintForGitlabCom < Gitlab::Database::Migration[2.1] + include Gitlab::Database::MigrationHelpers::ConvertToBigint + + disable_ddl_transaction! + + TABLE_NAME = :notes + INDEX_NAME = :index_notes_on_id_convert_to_bigint + + def up + return unless should_run? + + # This was created async for GitLab.com with + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119913 + # and will replace the existing PK index when we swap the integer and bigint columns in + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119705 + add_concurrent_index TABLE_NAME, :id_convert_to_bigint, + unique: true, + name: INDEX_NAME + end + + def down + return unless should_run? + + remove_concurrent_index_by_name(TABLE_NAME, INDEX_NAME) + end + + private + + def should_run? + com_or_dev_or_test_but_not_jh? + end +end diff --git a/db/post_migrate/20230516033729_add_referencing_bigint_fks_for_notes_on_gitlab_com.rb b/db/post_migrate/20230516033729_add_referencing_bigint_fks_for_notes_on_gitlab_com.rb new file mode 100644 index 00000000000..0cc77d43625 --- /dev/null +++ b/db/post_migrate/20230516033729_add_referencing_bigint_fks_for_notes_on_gitlab_com.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +class AddReferencingBigintFksForNotesOnGitlabCom < Gitlab::Database::Migration[2.1] + include Gitlab::Database::MigrationHelpers::ConvertToBigint + + disable_ddl_transaction! + + REFERENCING_FOREIGN_KEYS = [ + [:todos, :fk_91d1f47b13, :note_id, :cascade], + [:incident_management_timeline_events, :fk_d606a2a890, :promoted_from_note_id, :nullify], + [:system_note_metadata, :fk_d83a918cb1, :note_id, :cascade], + [:diff_note_positions, :fk_rails_13c7212859, :note_id, :cascade], + [:epic_user_mentions, :fk_rails_1c65976a49, :note_id, :cascade], + [:suggestions, :fk_rails_33b03a535c, :note_id, :cascade], + [:issue_user_mentions, :fk_rails_3861d9fefa, :note_id, :cascade], + [:note_diff_files, :fk_rails_3d66047aeb, :diff_note_id, :cascade], + [:snippet_user_mentions, :fk_rails_4d3f96b2cb, :note_id, :cascade], + [:design_user_mentions, :fk_rails_8de8c6d632, :note_id, :cascade], + [:vulnerability_user_mentions, :fk_rails_a18600f210, :note_id, :cascade], + [:commit_user_mentions, :fk_rails_a6760813e0, :note_id, :cascade], + [:merge_request_user_mentions, :fk_rails_c440b9ea31, :note_id, :cascade], + [:note_metadata, :fk_rails_d853224d37, :note_id, :cascade], + [:alert_management_alert_user_mentions, :fk_rails_eb2de0cdef, :note_id, :cascade], + [:timelogs, :fk_timelogs_note_id, :note_id, :nullify] + ] + + def up + return unless should_run? + + REFERENCING_FOREIGN_KEYS.each do |(from_table, name, column, on_delete)| + temporary_name = "#{name}_tmp" + + # This will replace the existing FKs when + # we swap the integer and bigint columns in + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119705 + add_concurrent_foreign_key( + from_table, + :notes, + column: column, + target_column: :id_convert_to_bigint, + name: temporary_name, + on_delete: on_delete, + reverse_lock_order: true, + validate: false) + + prepare_async_foreign_key_validation from_table, column, name: temporary_name + end + end + + def down + return unless should_run? + + REFERENCING_FOREIGN_KEYS.each do |(from_table, name, column, _)| + temporary_name = "#{name}_tmp" + + unprepare_async_foreign_key_validation from_table, column, name: temporary_name + + with_lock_retries do + remove_foreign_key_if_exists( + from_table, + :notes, + name: temporary_name, + reverse_lock_order: true + ) + end + end + end + + private + + def should_run? + com_or_dev_or_test_but_not_jh? + end +end diff --git a/db/schema_migrations/20230516032545 b/db/schema_migrations/20230516032545 new file mode 100644 index 00000000000..d9c5caaa3fd --- /dev/null +++ b/db/schema_migrations/20230516032545 @@ -0,0 +1 @@ +5f7e7d5b4af1a2e022e64ba2098c9e6be15853b2242334a41b4d53c5454201fe \ No newline at end of file diff --git a/db/schema_migrations/20230516033729 b/db/schema_migrations/20230516033729 new file mode 100644 index 00000000000..128f8154d29 --- /dev/null +++ b/db/schema_migrations/20230516033729 @@ -0,0 +1 @@ +ab3a2fa247fa1170aa38ec0471f136b479d715137138929a4d690f7b6022d022 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index ed00829cb6e..013b0e67c9a 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -31559,6 +31559,8 @@ CREATE INDEX index_notes_on_created_at ON notes USING btree (created_at); CREATE INDEX index_notes_on_discussion_id ON notes USING btree (discussion_id); +CREATE UNIQUE INDEX index_notes_on_id_convert_to_bigint ON notes USING btree (id_convert_to_bigint); + CREATE INDEX index_notes_on_id_where_confidential ON notes USING btree (id) WHERE (confidential = true); CREATE INDEX index_notes_on_id_where_internal ON notes USING btree (id) WHERE (internal = true); @@ -35156,6 +35158,9 @@ ALTER TABLE ONLY protected_tags ALTER TABLE ONLY todos ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY todos + ADD CONSTRAINT fk_91d1f47b13_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY dast_site_profiles_builds ADD CONSTRAINT fk_94e80df60e FOREIGN KEY (dast_site_profile_id) REFERENCES dast_site_profiles(id) ON DELETE CASCADE; @@ -35447,6 +35452,9 @@ ALTER TABLE ONLY ci_sources_pipelines ALTER TABLE ONLY incident_management_timeline_events ADD CONSTRAINT fk_d606a2a890 FOREIGN KEY (promoted_from_note_id) REFERENCES notes(id) ON DELETE SET NULL; +ALTER TABLE ONLY incident_management_timeline_events + ADD CONSTRAINT fk_d606a2a890_tmp FOREIGN KEY (promoted_from_note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE SET NULL NOT VALID; + ALTER TABLE ONLY lists ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; @@ -35468,6 +35476,9 @@ ALTER TABLE ONLY ci_pipelines ALTER TABLE ONLY system_note_metadata ADD CONSTRAINT fk_d83a918cb1 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY system_note_metadata + ADD CONSTRAINT fk_d83a918cb1_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY sbom_occurrences ADD CONSTRAINT fk_d857c6edc1 FOREIGN KEY (component_id) REFERENCES sbom_components(id) ON DELETE CASCADE; @@ -35783,6 +35794,9 @@ ALTER TABLE ONLY bulk_imports ALTER TABLE ONLY diff_note_positions ADD CONSTRAINT fk_rails_13c7212859 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY diff_note_positions + ADD CONSTRAINT fk_rails_13c7212859_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY analytics_cycle_analytics_aggregations ADD CONSTRAINT fk_rails_13c8374c7a FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -35846,6 +35860,9 @@ ALTER TABLE ONLY board_assignees ALTER TABLE ONLY epic_user_mentions ADD CONSTRAINT fk_rails_1c65976a49 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY epic_user_mentions + ADD CONSTRAINT fk_rails_1c65976a49_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY approver_groups ADD CONSTRAINT fk_rails_1cdcbd7723 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -36005,6 +36022,9 @@ ALTER TABLE ONLY alert_management_alert_metric_images ALTER TABLE ONLY suggestions ADD CONSTRAINT fk_rails_33b03a535c FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY suggestions + ADD CONSTRAINT fk_rails_33b03a535c_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY requirements ADD CONSTRAINT fk_rails_33fed8aa4e FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL; @@ -36035,6 +36055,9 @@ ALTER TABLE ONLY packages_debian_project_distribution_keys ALTER TABLE ONLY issue_user_mentions ADD CONSTRAINT fk_rails_3861d9fefa FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY issue_user_mentions + ADD CONSTRAINT fk_rails_3861d9fefa_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY namespace_settings ADD CONSTRAINT fk_rails_3896d4fae5 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -36062,6 +36085,9 @@ ALTER TABLE ONLY cluster_groups ALTER TABLE ONLY note_diff_files ADD CONSTRAINT fk_rails_3d66047aeb FOREIGN KEY (diff_note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY note_diff_files + ADD CONSTRAINT fk_rails_3d66047aeb_tmp FOREIGN KEY (diff_note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY snippet_user_mentions ADD CONSTRAINT fk_rails_3e00189191 FOREIGN KEY (snippet_id) REFERENCES snippets(id) ON DELETE CASCADE; @@ -36167,6 +36193,9 @@ ALTER TABLE ONLY scim_identities ALTER TABLE ONLY snippet_user_mentions ADD CONSTRAINT fk_rails_4d3f96b2cb FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY snippet_user_mentions + ADD CONSTRAINT fk_rails_4d3f96b2cb_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY protected_environment_approval_rules ADD CONSTRAINT fk_rails_4e554f96f5 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE; @@ -36596,6 +36625,9 @@ ALTER TABLE ONLY approval_merge_request_rules_approved_approvers ALTER TABLE ONLY design_user_mentions ADD CONSTRAINT fk_rails_8de8c6d632 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY design_user_mentions + ADD CONSTRAINT fk_rails_8de8c6d632_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY clusters_kubernetes_namespaces ADD CONSTRAINT fk_rails_8df789f3ab FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE SET NULL; @@ -36725,6 +36757,9 @@ ALTER TABLE ONLY project_aliases ALTER TABLE ONLY vulnerability_user_mentions ADD CONSTRAINT fk_rails_a18600f210 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY vulnerability_user_mentions + ADD CONSTRAINT fk_rails_a18600f210_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY todos ADD CONSTRAINT fk_rails_a27c483435 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; @@ -36752,6 +36787,9 @@ ALTER TABLE ONLY cluster_projects ALTER TABLE ONLY commit_user_mentions ADD CONSTRAINT fk_rails_a6760813e0 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY commit_user_mentions + ADD CONSTRAINT fk_rails_a6760813e0_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY vulnerability_identifiers ADD CONSTRAINT fk_rails_a67a16c885 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; @@ -36962,6 +37000,9 @@ ALTER TABLE ONLY project_wiki_repositories ALTER TABLE ONLY merge_request_user_mentions ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY merge_request_user_mentions + ADD CONSTRAINT fk_rails_c440b9ea31_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY user_achievements ADD CONSTRAINT fk_rails_c44f5b3b25 FOREIGN KEY (achievement_id) REFERENCES achievements(id) ON DELETE CASCADE; @@ -37079,6 +37120,9 @@ ALTER TABLE ONLY packages_rpm_metadata ALTER TABLE ONLY note_metadata ADD CONSTRAINT fk_rails_d853224d37 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY note_metadata + ADD CONSTRAINT fk_rails_d853224d37_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY merge_request_reviewers ADD CONSTRAINT fk_rails_d9fec24b9d FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; @@ -37220,6 +37264,9 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels ALTER TABLE ONLY alert_management_alert_user_mentions ADD CONSTRAINT fk_rails_eb2de0cdef FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE; +ALTER TABLE ONLY alert_management_alert_user_mentions + ADD CONSTRAINT fk_rails_eb2de0cdef_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID; + ALTER TABLE ONLY snippet_statistics ADD CONSTRAINT fk_rails_ebc283ccf1 FOREIGN KEY (snippet_id) REFERENCES snippets(id) ON DELETE CASCADE; @@ -37370,6 +37417,9 @@ ALTER TABLE ONLY timelogs ALTER TABLE ONLY timelogs ADD CONSTRAINT fk_timelogs_note_id FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE SET NULL; +ALTER TABLE ONLY timelogs + ADD CONSTRAINT fk_timelogs_note_id_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE SET NULL NOT VALID; + ALTER TABLE ONLY u2f_registrations ADD CONSTRAINT fk_u2f_registrations_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/doc/ci/cloud_services/google_cloud/index.md b/doc/ci/cloud_services/google_cloud/index.md index 5ed22883518..d99b50b5013 100644 --- a/doc/ci/cloud_services/google_cloud/index.md +++ b/doc/ci/cloud_services/google_cloud/index.md @@ -114,6 +114,17 @@ the assertion in the previous section. After you configure the OIDC and role, the GitLab CI/CD job can retrieve a temporary credential from the [Google Cloud Security Token Service (STS)](https://cloud.google.com/iam/docs/reference/sts/rest). +Add `id_tokens` to your CI/CD job: + +```yaml +job: + id_tokens: + GITLAB_OIDC_TOKEN: + aud: https://gitlab.example.com +``` + +Get temporary credentials using the ID token: + ```shell PAYLOAD="$(cat <1 | [Javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz) (recommended) | [javafuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/javafuzz-fuzzing-example) | | Java | [JQF](https://github.com/rohanpadhye/JQF) (not preferred) | [jqf-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/java-fuzzing-example) | | JavaScript | [`jsfuzz`](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz) | [jsfuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/jsfuzz-fuzzing-example) | | Python | [`pythonfuzz`](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/pythonfuzz) | [pythonfuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/pythonfuzz-fuzzing-example) | | AFL (any language that works on top of AFL) | [AFL](https://lcamtuf.coredump.cx/afl/) | [afl-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/afl-fuzzing-example) | +1. Support for Gradle is planned in [issue 409764](https://gitlab.com/gitlab-org/gitlab/-/issues/409764). + ## Confirm status of coverage-guided fuzz testing To confirm the status of coverage-guided fuzz testing: diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 27dc7af7e5c..61bb794f7d9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -11098,9 +11098,6 @@ msgstr "" msgid "Committed-before" msgstr "" -msgid "CommonJS module" -msgstr "" - msgid "Community forum" msgstr "" @@ -16185,9 +16182,6 @@ msgstr "" msgid "E-mail:" msgstr "" -msgid "ESM module" -msgstr "" - msgid "Each project can also have an issue tracker and a wiki." msgstr "" @@ -21779,9 +21773,6 @@ msgstr "" msgid "HAR file path or URL" msgstr "" -msgid "HTML script tag" -msgstr "" - msgid "HTTP Archive (HAR)" msgstr "" @@ -34090,15 +34081,27 @@ msgstr "" msgid "Product analytics" msgstr "" +msgid "ProductAnalytics|1. Add the NPM package to your package.json using your preferred package manager" +msgstr "" + +msgid "ProductAnalytics|2. Import the new package into your JS code" +msgstr "" + +msgid "ProductAnalytics|3. Initiate the tracking" +msgstr "" + msgid "ProductAnalytics|Add another dimension" msgstr "" -msgid "ProductAnalytics|Add the NPM package to your package.json using your preferred package manager:" +msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window" msgstr "" msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window:" msgstr "" +msgid "ProductAnalytics|After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}." +msgstr "" + msgid "ProductAnalytics|All Clicks Compared" msgstr "" @@ -34198,12 +34201,6 @@ msgstr "" msgid "ProductAnalytics|How often sessions are repeated" msgstr "" -msgid "ProductAnalytics|Identifies the sender of tracking events" -msgstr "" - -msgid "ProductAnalytics|Import the new package into your JS code:" -msgstr "" - msgid "ProductAnalytics|Instrument your application" msgstr "" @@ -34234,10 +34231,13 @@ msgstr "" msgid "ProductAnalytics|Repeat Visit Percentage" msgstr "" -msgid "ProductAnalytics|SDK App ID" +msgid "ProductAnalytics|SDK application ID" +msgstr "" + +msgid "ProductAnalytics|SDK clients" msgstr "" -msgid "ProductAnalytics|SDK Host" +msgid "ProductAnalytics|SDK host" msgstr "" msgid "ProductAnalytics|Sessions" @@ -34252,16 +34252,10 @@ msgstr "" msgid "ProductAnalytics|Set up to track how your product is performing and optimize your product and development processes." msgstr "" -msgid "ProductAnalytics|Steps to add product analytics as a CommonJS module" -msgstr "" - -msgid "ProductAnalytics|Steps to add product analytics as a HTML script tag" -msgstr "" - -msgid "ProductAnalytics|Steps to add product analytics as an ESM module" +msgid "ProductAnalytics|The host to send all tracking events to" msgstr "" -msgid "ProductAnalytics|The host to send all tracking events to" +msgid "ProductAnalytics|The sender of tracking events" msgstr "" msgid "ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already." @@ -34288,12 +34282,18 @@ msgstr "" msgid "ProductAnalytics|Users" msgstr "" +msgid "ProductAnalytics|Using JS module" +msgstr "" + msgid "ProductAnalytics|Waiting for events" msgstr "" msgid "ProductAnalytics|What do you want to measure?" msgstr "" +msgid "ProductAnalytics|You can instrument your application using a JS module or an HTML script. Follow the instructions below for the option you prefer." +msgstr "" + msgid "Productivity" msgstr "" @@ -35314,10 +35314,10 @@ msgstr "" msgid "ProjectSettings|Configure analytics features for this project" msgstr "" -msgid "ProjectSettings|Configure product analytics to track events within your project applications." +msgid "ProjectSettings|Configure your infrastructure." msgstr "" -msgid "ProjectSettings|Configure your infrastructure." +msgid "ProjectSettings|Connect to your instance" msgstr "" msgid "ProjectSettings|Contact an admin to change this setting." @@ -35419,6 +35419,9 @@ msgstr "" msgid "ProjectSettings|Infrastructure" msgstr "" +msgid "ProjectSettings|Instrument your application to track events and behaviors." +msgstr "" + msgid "ProjectSettings|Internal" msgstr "" @@ -35503,6 +35506,9 @@ msgstr "" msgid "ProjectSettings|Only signed commits can be pushed to this repository." msgstr "" +msgid "ProjectSettings|Override instance analytics configuration for this project" +msgstr "" + msgid "ProjectSettings|Override user notification preferences for all project members." msgstr "" @@ -35521,6 +35527,9 @@ msgstr "" msgid "ProjectSettings|Private" msgstr "" +msgid "ProjectSettings|Product analytics needs to be set up before your application can be instrumented. Follow the %{link_start}set up process%{link_end}." +msgstr "" + msgid "ProjectSettings|Project visibility" msgstr "" @@ -49054,6 +49063,9 @@ msgstr "" msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}" msgstr "" +msgid "Using HTML script" +msgstr "" + msgid "Using required encryption strategy when encrypted field is missing!" msgstr "" diff --git a/rubocop/cop/graphql/resource_not_available_error.rb b/rubocop/cop/graphql/resource_not_available_error.rb new file mode 100644 index 00000000000..d759e145008 --- /dev/null +++ b/rubocop/cop/graphql/resource_not_available_error.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require_relative '../../node_pattern_helper' + +module RuboCop + module Cop + module Graphql + # Encourages the use of `raise_resource_not_available_error!` method + # instead of `raise Gitlab::Graphql::Errors::ResourceNotAvailable`. + # + # @example + # + # # bad + # raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'message' + # + # # good + # raise_resource_not_available_error! 'message' + class ResourceNotAvailableError < Base + extend NodePatternHelper + extend AutoCorrector + + MSG = 'Prefer using `raise_resource_not_available_error!` instead.' + + EXCEPTION = 'Gitlab::Graphql::Errors::ResourceNotAvailable' + + RESTRICT_ON_SEND = %i[raise].freeze + + def_node_matcher :error, const_pattern(EXCEPTION) + + def_node_matcher :raise_error, <<~PATTERN + (send nil? :raise #error $...) + PATTERN + + def on_send(node) + raise_error(node) do |args| + add_offense(node) do |corrector| + replacement = +'raise_resource_not_available_error!' + replacement << " #{args.map(&:source).join(', ')}" if args.any? + + corrector.replace(node, replacement) + end + end + end + end + end + end +end diff --git a/rubocop/node_pattern_helper.rb b/rubocop/node_pattern_helper.rb new file mode 100644 index 00000000000..ecd4560763c --- /dev/null +++ b/rubocop/node_pattern_helper.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module RuboCop + module NodePatternHelper + # Returns a nested `(const ...)` node pattern for a full qualified +name+. + # + # @examples + # const_pattern 'Foo::Bar' # => (const (const {nil? cbase} :Foo) :Bar) + # const_pattern 'Foo::Bar', parent: ':Baz' # => (const (const :Baz :Foo) :Bar) + def const_pattern(name, parent: '{nil? cbase}') + name.split('::').inject(parent) { |memo, name_part| "(const #{memo} :#{name_part})" } + end + end +end diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb index ec74a902258..e60cf37aad6 100644 --- a/spec/controllers/sent_notifications_controller_spec.rb +++ b/spec/controllers/sent_notifications_controller_spec.rb @@ -7,10 +7,12 @@ RSpec.describe SentNotificationsController do let(:project) { create(:project, :public) } let(:private_project) { create(:project, :private) } let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: user) } + let(:email) { 'email@example.com' } let(:issue) do - create(:issue, project: target_project) do |issue| + create(:issue, project: target_project, external_author: email) do |issue| issue.subscriptions.create!(user: user, project: target_project, subscribed: true) + issue.issue_email_participants.create!(email: email) end end @@ -29,6 +31,14 @@ RSpec.describe SentNotificationsController do let(:noteable) { issue } let(:target_project) { project } + def force_unsubscribe + get(:unsubscribe, params: { id: sent_notification.reply_key, force: true }) + end + + def unsubscribe + get(:unsubscribe, params: { id: sent_notification.reply_key }) + end + describe 'GET unsubscribe' do shared_examples 'returns 404' do it 'does not set the flash message' do @@ -43,13 +53,17 @@ RSpec.describe SentNotificationsController do context 'when the user is not logged in' do context 'when the force param is passed' do before do - get(:unsubscribe, params: { id: sent_notification.reply_key, force: true }) + force_unsubscribe end it 'unsubscribes the user' do expect(issue.subscribed?(user, project)).to be_falsey end + it 'does not delete the issue email participant for non-service-desk issue' do + expect { force_unsubscribe }.not_to change { issue.issue_email_participants.count } + end + it 'sets the flash message' do expect(controller).to set_flash[:notice].to(/unsubscribed/) end @@ -63,7 +77,7 @@ RSpec.describe SentNotificationsController do render_views before do - get(:unsubscribe, params: { id: sent_notification.reply_key }) + unsubscribe end shared_examples 'unsubscribing as anonymous' do |project_visibility| @@ -101,6 +115,10 @@ RSpec.describe SentNotificationsController do expect(response.body).to include(issue.title) end + it 'does not delete the issue email participant' do + expect { unsubscribe }.not_to change { issue.issue_email_participants.count } + end + it_behaves_like 'unsubscribing as anonymous', :public end @@ -171,7 +189,7 @@ RSpec.describe SentNotificationsController do before do sent_notification.noteable.destroy! - get(:unsubscribe, params: { id: sent_notification.reply_key }) + unsubscribe end it_behaves_like 'returns 404' @@ -193,7 +211,7 @@ RSpec.describe SentNotificationsController do context 'when the force param is passed' do before do - get(:unsubscribe, params: { id: sent_notification.reply_key, force: true }) + force_unsubscribe end it 'unsubscribes the user' do @@ -220,7 +238,7 @@ RSpec.describe SentNotificationsController do let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) } before do - get(:unsubscribe, params: { id: sent_notification.reply_key }) + unsubscribe end it 'unsubscribes the user' do @@ -243,7 +261,7 @@ RSpec.describe SentNotificationsController do let(:target_project) { private_project } before do - get(:unsubscribe, params: { id: sent_notification.reply_key }) + unsubscribe end it 'unsubscribes user and redirects to root path' do @@ -257,12 +275,16 @@ RSpec.describe SentNotificationsController do before do private_project.add_developer(user) - get(:unsubscribe, params: { id: sent_notification.reply_key }) + unsubscribe end it 'unsubscribes user and redirects to issue path' do expect(response).to redirect_to(project_issue_path(private_project, issue)) end + + it 'does not delete the issue email participant for non-service-desk issue' do + expect { unsubscribe }.not_to change { issue.issue_email_participants.count } + end end end @@ -270,11 +292,27 @@ RSpec.describe SentNotificationsController do before do sent_notification.noteable.destroy! - get(:unsubscribe, params: { id: sent_notification.reply_key }) + unsubscribe end it_behaves_like 'returns 404' end + + context 'when support bot is the notification recipient' do + let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: User.support_bot) } + + it 'deletes the external author on the issue' do + expect { unsubscribe }.to change { issue.issue_email_participants.count }.by(-1) + end + + context 'when noteable is not an issue' do + let(:noteable) { merge_request } + + it 'does not delete the external author on the issue' do + expect { unsubscribe }.not_to change { issue.issue_email_participants.count } + end + end + end end end end diff --git a/spec/frontend/layout_nav_spec.js b/spec/frontend/layout_nav_spec.js new file mode 100644 index 00000000000..30f4f7fcac1 --- /dev/null +++ b/spec/frontend/layout_nav_spec.js @@ -0,0 +1,39 @@ +import { initScrollingTabs } from '~/layout_nav'; +import { setHTMLFixture } from './__helpers__/fixtures'; + +describe('initScrollingTabs', () => { + const htmlFixture = ` + + +
+ `; + const findTabs = () => document.querySelector('.scrolling-tabs'); + const findScrollLeftButton = () => document.querySelector('button.fade-left'); + const findScrollRightButton = () => document.querySelector('button.fade-right'); + + beforeEach(() => { + setHTMLFixture(htmlFixture); + }); + + it('scrolls left when clicking on the left button', () => { + initScrollingTabs(); + const tabs = findTabs(); + tabs.scrollBy = jest.fn(); + const fadeLeft = findScrollLeftButton(); + + fadeLeft.click(); + + expect(tabs.scrollBy).toHaveBeenCalledWith({ left: -200, behavior: 'smooth' }); + }); + + it('scrolls right when clicking on the right button', () => { + initScrollingTabs(); + const tabs = findTabs(); + tabs.scrollBy = jest.fn(); + const fadeRight = findScrollRightButton(); + + fadeRight.click(); + + expect(tabs.scrollBy).toHaveBeenCalledWith({ left: 200, behavior: 'smooth' }); + }); +}); diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 6ae33fe2642..a3d015fd768 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -2002,6 +2002,30 @@ RSpec.describe Issue, feature_category: :team_planning do it { is_expected.to eq(WorkItems::Type.default_by_type(::Issue::DEFAULT_ISSUE_TYPE)) } end + describe '#unsubscribe_email_participant' do + let_it_be(:email) { 'email@example.com' } + + let_it_be(:issue1) do + create(:issue, project: reusable_project, external_author: email) do |issue| + issue.issue_email_participants.create!(email: email) + end + end + + let_it_be(:issue2) do + create(:issue, project: reusable_project, external_author: email) do |issue| + issue.issue_email_participants.create!(email: email) + end + end + + it 'deletes email for issue1' do + expect { issue1.unsubscribe_email_participant(email) }.to change { issue1.issue_email_participants.count }.by(-1) + end + + it 'does not delete email for issue2 when issue1 is used' do + expect { issue1.unsubscribe_email_participant(email) }.not_to change { issue2.issue_email_participants.count } + end + end + describe 'issue_type enum generated methods' do using RSpec::Parameterized::TableSyntax diff --git a/spec/rubocop/cop/graphql/resource_not_available_error_spec.rb b/spec/rubocop/cop/graphql/resource_not_available_error_spec.rb new file mode 100644 index 00000000000..6003b9f3954 --- /dev/null +++ b/spec/rubocop/cop/graphql/resource_not_available_error_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../../../rubocop/cop/graphql/resource_not_available_error' + +RSpec.describe RuboCop::Cop::Graphql::ResourceNotAvailableError, feature_category: :shared do + shared_examples 'flagging and auto-correction' do |exception| + it "flags and auto-corrects `raise #{exception}`" do + expect_offense(<<~'RUBY', exception: exception) + raise %{exception} + ^^^^^^^{exception} Prefer using `raise_resource_not_available_error!` instead. + + raise %{exception}, 'message ' \ + ^^^^^^^{exception}^^^^^^^^^^^^^^ Prefer using `raise_resource_not_available_error!` instead. + 'with new lines' + RUBY + + expect_correction(<<~'RUBY') + raise_resource_not_available_error! + + raise_resource_not_available_error! 'message ' \ + 'with new lines' + RUBY + end + end + + it_behaves_like 'flagging and auto-correction', 'Gitlab::Graphql::Errors::ResourceNotAvailable' + it_behaves_like 'flagging and auto-correction', '::Gitlab::Graphql::Errors::ResourceNotAvailable' + + it 'does not flag unrelated exceptions' do + expect_no_offenses(<<~RUBY) + raise Gitlab::Graphql::Errors::ResourceVeryAvailable + raise ::Gitlab::Graphql::Errors::ResourceVeryAvailable + RUBY + end +end diff --git a/spec/rubocop/node_pattern_helper_spec.rb b/spec/rubocop/node_pattern_helper_spec.rb new file mode 100644 index 00000000000..a141e81b618 --- /dev/null +++ b/spec/rubocop/node_pattern_helper_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../rubocop/node_pattern_helper' + +RSpec.describe RuboCop::NodePatternHelper, feature_category: :tooling do + include described_class + + describe '#const_pattern' do + it 'returns nested const node patterns' do + expect(const_pattern('Foo')).to eq('(const {nil? cbase} :Foo)') + expect(const_pattern('Foo::Bar')).to eq('(const (const {nil? cbase} :Foo) :Bar)') + end + + it 'returns nested const node patterns with custom parent' do + expect(const_pattern('Foo::Bar', parent: 'nil?')).to eq('(const (const nil? :Foo) :Bar)') + end + end +end diff --git a/spec/services/ci/job_artifacts/create_service_spec.rb b/spec/services/ci/job_artifacts/create_service_spec.rb index f71d7feb04a..73cb791f145 100644 --- a/spec/services/ci/job_artifacts/create_service_spec.rb +++ b/spec/services/ci/job_artifacts/create_service_spec.rb @@ -82,7 +82,11 @@ RSpec.describe Ci::JobArtifacts::CreateService, :clean_gitlab_redis_shared_state before do stub_artifacts_object_storage(JobArtifactUploader, direct_upload: true) - allow(JobArtifactUploader).to receive(:generate_final_store_path).and_return(final_store_path) + + allow(JobArtifactUploader) + .to receive(:generate_final_store_path) + .with(root_id: project.id) + .and_return(final_store_path) end it 'includes the authorize headers' do diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 1566021934a..a748c544bfd 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -525,12 +525,14 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category let(:has_length) { true } let(:maximum_size) { nil } let(:use_final_store_path) { false } + let(:final_store_path_root_id) { nil } subject do uploader_class.workhorse_authorize( has_length: has_length, maximum_size: maximum_size, - use_final_store_path: use_final_store_path + use_final_store_path: use_final_store_path, + final_store_path_root_id: final_store_path_root_id ) end @@ -615,51 +617,30 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category shared_examples 'handling object storage final upload path' do |multipart| context 'when use_final_store_path is true' do let(:use_final_store_path) { true } - let(:final_store_path) { File.join('@final', 'abc', '123', 'somefilename') } + let(:final_store_path_root_id) { 12345 } + let(:final_store_path) { File.join('@final', 'myprefix', 'abc', '123', 'somefilename') } let(:escaped_path) { escape_path(final_store_path) } - before do - stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{final_store_path}") if multipart - - allow(uploader_class).to receive(:generate_final_store_path).and_return(final_store_path) - end - - it 'uses the full path instead of the temporary one' do - expect(subject[:RemoteObject][:ID]).to eq(final_store_path) + context 'and final_store_path_root_id was not given' do + let(:final_store_path_root_id) { nil } - expect(subject[:RemoteObject][:GetURL]).to include(escaped_path) - expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path) - - if multipart - expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(escaped_path)) - expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path) - expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path) + it 'raises an error' do + expect { subject }.to raise_error(ObjectStorage::MissingFinalStorePathRootId) end - - expect(subject[:RemoteObject][:SkipDelete]).to eq(true) - - expect( - ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path) - ).to eq(true) end - context 'and bucket prefix is configured' do - let(:prefixed_final_store_path) { "my/prefix/#{final_store_path}" } - let(:escaped_path) { escape_path(prefixed_final_store_path) } - + context 'and final_store_path_root_id was given' do before do - allow(uploader_class.object_store_options).to receive(:bucket_prefix).and_return('my/prefix') + stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{final_store_path}") if multipart - if multipart - stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{prefixed_final_store_path}") - end + allow(uploader_class).to receive(:generate_final_store_path) + .with(root_id: final_store_path_root_id) + .and_return(final_store_path) end - it 'sets the remote object ID to the final path without prefix' do + it 'uses the full path instead of the temporary one' do expect(subject[:RemoteObject][:ID]).to eq(final_store_path) - end - it 'returns the final path with prefix' do expect(subject[:RemoteObject][:GetURL]).to include(escaped_path) expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path) @@ -668,15 +649,49 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path) expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path) end - end - it 'creates the pending upload entry without the prefix' do - is_expected.to have_key(:RemoteObject) + expect(subject[:RemoteObject][:SkipDelete]).to eq(true) expect( ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path) ).to eq(true) end + + context 'and bucket prefix is configured' do + let(:prefixed_final_store_path) { "my/prefix/#{final_store_path}" } + let(:escaped_path) { escape_path(prefixed_final_store_path) } + + before do + allow(uploader_class.object_store_options).to receive(:bucket_prefix).and_return('my/prefix') + + if multipart + stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{prefixed_final_store_path}") + end + end + + it 'sets the remote object ID to the final path without prefix' do + expect(subject[:RemoteObject][:ID]).to eq(final_store_path) + end + + it 'returns the final path with prefix' do + expect(subject[:RemoteObject][:GetURL]).to include(escaped_path) + expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path) + + if multipart + expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(escaped_path)) + expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path) + expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path) + end + end + + it 'creates the pending upload entry without the bucket prefix' do + is_expected.to have_key(:RemoteObject) + + expect( + ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path) + ).to eq(true) + end + end end end @@ -716,7 +731,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category end before do - expect_next_instance_of(ObjectStorage::Config) do |instance| + allow_next_instance_of(ObjectStorage::Config) do |instance| allow(instance).to receive(:credentials).and_return(credentials) end end @@ -767,7 +782,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category end before do - expect_next_instance_of(ObjectStorage::Config) do |instance| + allow_next_instance_of(ObjectStorage::Config) do |instance| allow(instance).to receive(:credentials).and_return(credentials) end end @@ -812,7 +827,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category end before do - expect_next_instance_of(ObjectStorage::Config) do |instance| + allow_next_instance_of(ObjectStorage::Config) do |instance| allow(instance).to receive(:credentials).and_return(credentials) end end @@ -1184,14 +1199,17 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category end describe '.generate_final_store_path' do - subject(:final_path) { uploader_class.generate_final_store_path } + let(:root_id) { 12345 } + let(:expected_root_hashed_path) { Gitlab::HashedPath.new(root_hash: root_id) } + + subject(:final_path) { uploader_class.generate_final_store_path(root_id: root_id) } before do allow(Digest::SHA2).to receive(:hexdigest).and_return('somehash1234') end - it 'returns the generated hashed path' do - expect(final_path).to eq('@final/so/me/hash1234') + it 'returns the generated hashed path nested under the hashed path of the root ID' do + expect(final_path).to eq(File.join(expected_root_hashed_path, '@final/so/me/hash1234')) end end -- cgit v1.2.1