Add latest changes from gitlab-org/gitlab@master
|  | @ -540,7 +540,6 @@ Layout/LineLength: | |||
|     - 'ee/app/helpers/ee/mirror_helper.rb' | ||||
|     - 'ee/app/helpers/ee/notes_helper.rb' | ||||
|     - 'ee/app/helpers/ee/profiles_helper.rb' | ||||
|     - 'ee/app/helpers/ee/projects_helper.rb' | ||||
|     - 'ee/app/helpers/ee/subscribable_banner_helper.rb' | ||||
|     - 'ee/app/helpers/epics_helper.rb' | ||||
|     - 'ee/app/helpers/gitlab_subscriptions/upcoming_reconciliation_helper.rb' | ||||
|  | @ -1163,7 +1162,6 @@ Layout/LineLength: | |||
|     - 'ee/spec/helpers/projects/on_demand_scans_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects/project_members_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/push_rules_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/subscriptions_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/timeboxes_helper_spec.rb' | ||||
|  | @ -1960,7 +1958,6 @@ Layout/LineLength: | |||
|     - 'lib/api/project_snippets.rb' | ||||
|     - 'lib/api/project_templates.rb' | ||||
|     - 'lib/api/projects.rb' | ||||
|     - 'lib/api/projects_relation_builder.rb' | ||||
|     - 'lib/api/pypi_packages.rb' | ||||
|     - 'lib/api/releases.rb' | ||||
|     - 'lib/api/repositories.rb' | ||||
|  |  | |||
|  | @ -259,7 +259,6 @@ Lint/UnusedMethodArgument: | |||
|     - 'lib/api/helpers.rb' | ||||
|     - 'lib/api/helpers/notes_helpers.rb' | ||||
|     - 'lib/api/merge_requests.rb' | ||||
|     - 'lib/api/projects_relation_builder.rb' | ||||
|     - 'lib/atlassian/jira_connect/client.rb' | ||||
|     - 'lib/banzai/filter/playable_link_filter.rb' | ||||
|     - 'lib/banzai/filter/references/abstract_reference_filter.rb' | ||||
|  |  | |||
|  | @ -252,7 +252,6 @@ RSpec/BeforeAllRoleAssignment: | |||
|     - 'ee/spec/helpers/gitlab_subscriptions/upcoming_reconciliation_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects/on_demand_scans_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects/project_members_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/subscriptions_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/timeboxes_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/tree_helper_spec.rb' | ||||
|  |  | |||
|  | @ -251,7 +251,6 @@ RSpec/ContextWording: | |||
|     - 'ee/spec/helpers/ee/personal_access_tokens_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/ee/projects/security/api_fuzzing_configuration_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/license_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/roadmaps_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/security_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/subscriptions_helper_spec.rb' | ||||
|  |  | |||
|  | @ -41,7 +41,6 @@ RSpec/ExampleWithoutDescription: | |||
|     - 'ee/spec/helpers/ee/projects/security/dast_configuration_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/ee/projects/security/sast_configuration_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/ee/users/callouts_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/users_helper_spec.rb' | ||||
|     - 'ee/spec/lib/api/entities/clusters/receptive_agent_spec.rb' | ||||
|     - 'ee/spec/lib/ee/api/entities/experiment_spec.rb' | ||||
|  |  | |||
|  | @ -194,7 +194,6 @@ RSpec/NamedSubject: | |||
|     - 'ee/spec/helpers/kerberos_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/license_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/secrets_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/users_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/vulnerabilities_helper_spec.rb' | ||||
|  |  | |||
|  | @ -36,7 +36,6 @@ RSpec/ReceiveMessages: | |||
|     - 'ee/spec/helpers/license_monitoring_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/nav/new_dropdown_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects/project_members_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/projects_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/routing/pseudonymization_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/sidebars_helper_spec.rb' | ||||
|     - 'ee/spec/helpers/subscriptions_helper_spec.rb' | ||||
|  |  | |||
|  | @ -110,7 +110,6 @@ Style/FormatString: | |||
|     - 'ee/app/helpers/ee/groups_helper.rb' | ||||
|     - 'ee/app/helpers/ee/import_helper.rb' | ||||
|     - 'ee/app/helpers/ee/profiles_helper.rb' | ||||
|     - 'ee/app/helpers/ee/projects_helper.rb' | ||||
|     - 'ee/app/helpers/ee/timeboxes_helper.rb' | ||||
|     - 'ee/app/helpers/vulnerabilities_helper.rb' | ||||
|     - 'ee/app/mailers/ee/emails/admin_notification.rb' | ||||
|  |  | |||
|  | @ -220,7 +220,6 @@ Style/GuardClause: | |||
|     - 'ee/app/helpers/ee/auth_helper.rb' | ||||
|     - 'ee/app/helpers/ee/nav/new_dropdown_helper.rb' | ||||
|     - 'ee/app/helpers/ee/projects/pipeline_helper.rb' | ||||
|     - 'ee/app/helpers/ee/projects_helper.rb' | ||||
|     - 'ee/app/models/allowed_email_domain.rb' | ||||
|     - 'ee/app/models/app_sec/fuzzing/coverage/corpus.rb' | ||||
|     - 'ee/app/models/approval_merge_request_rule_source.rb' | ||||
|  |  | |||
|  | @ -235,7 +235,6 @@ Style/IfUnlessModifier: | |||
|     - 'ee/app/helpers/ee/merge_requests_helper.rb' | ||||
|     - 'ee/app/helpers/ee/notes_helper.rb' | ||||
|     - 'ee/app/helpers/ee/projects/pipeline_helper.rb' | ||||
|     - 'ee/app/helpers/ee/projects_helper.rb' | ||||
|     - 'ee/app/models/allowed_email_domain.rb' | ||||
|     - 'ee/app/models/app_sec/fuzzing/coverage/corpus.rb' | ||||
|     - 'ee/app/models/concerns/elastic/application_versioned_search.rb' | ||||
|  | @ -443,7 +442,6 @@ Style/IfUnlessModifier: | |||
|     - 'lib/api/pages_domains.rb' | ||||
|     - 'lib/api/project_snippets.rb' | ||||
|     - 'lib/api/projects.rb' | ||||
|     - 'lib/api/projects_relation_builder.rb' | ||||
|     - 'lib/api/protected_branches.rb' | ||||
|     - 'lib/api/repositories.rb' | ||||
|     - 'lib/api/rubygem_packages.rb' | ||||
|  |  | |||
|  | @ -11,7 +11,6 @@ Style/RedundantParentheses: | |||
|     - 'config/initializers/8_devise.rb' | ||||
|     - 'config/initializers/zz_metrics.rb' | ||||
|     - 'ee/app/controllers/groups/billings_controller.rb' | ||||
|     - 'ee/app/helpers/ee/projects_helper.rb' | ||||
|     - 'ee/app/models/push_rule.rb' | ||||
|     - 'ee/app/services/concerns/approval_rules/updater.rb' | ||||
|     - 'ee/app/services/merge_trains/refresh_service.rb' | ||||
|  |  | |||
|  | @ -31,14 +31,15 @@ export default { | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div class="gl-flex gl-grow gl-items-center gl-justify-end"> | ||||
|   <li role="presentation" class="gl-flex gl-grow gl-items-center gl-justify-end"> | ||||
|     <label id="database-selector-label" class="gl-sr-only">{{ __('Selected database') }}</label> | ||||
|     <gl-collapsible-listbox | ||||
|       v-model="selected" | ||||
|       :items="databases" | ||||
|       placement="bottom-end" | ||||
|       :toggle-text="selectedDatabase" | ||||
|       toggle-aria-labelled-by="label" | ||||
|       toggle-aria-labelled-by="database-selector-label" | ||||
|       @select="selectDatabase" | ||||
|     /> | ||||
|   </div> | ||||
|   </li> | ||||
| </template> | ||||
|  |  | |||
|  | @ -41,11 +41,12 @@ export default { | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <gl-form-group :label="$options.i18n.label"> | ||||
|   <gl-form-group id="organization-role-field" :label="$options.i18n.label"> | ||||
|     <gl-collapsible-listbox | ||||
|       v-model="accessLevel" | ||||
|       block | ||||
|       toggle-class="gl-form-input-xl" | ||||
|       toggle-aria-labelled-by="organization-role-field" | ||||
|       :items="$options.roleListboxItems" | ||||
|     /> | ||||
|     <input :id="$options.inputId" :name="inputName" :value="accessLevel" type="hidden" /> | ||||
|  |  | |||
|  | @ -84,8 +84,8 @@ export default { | |||
|       items.push({ | ||||
|         text: this.$options.i18n.delete, | ||||
|         action: this.onDelete, | ||||
|         variant: 'danger', | ||||
|         extraAttrs: { | ||||
|           class: '!gl-text-red-500', | ||||
|           'data-testid': `delete-label-action`, | ||||
|         }, | ||||
|       }); | ||||
|  |  | |||
|  | @ -138,9 +138,7 @@ export default function initHeaderApp({ router, isReadmeView = false, isBlobView | |||
|           props: { | ||||
|             refType, | ||||
|             currentRef: ref, | ||||
|             // BlobControls:
 | ||||
|             projectPath, | ||||
|             // RefSelector:
 | ||||
|             projectId, | ||||
|           }, | ||||
|         }); | ||||
|  |  | |||
|  | @ -668,7 +668,7 @@ export default { | |||
|         optimisticResponse: designUploadOptimisticResponse(this.filesToBeSaved), | ||||
|         variables: { | ||||
|           files: this.filesToBeSaved, | ||||
|           projectPath: this.fullPath, | ||||
|           projectPath: this.workItemFullPath, | ||||
|           iid: this.iid, | ||||
|         }, | ||||
|         context: { | ||||
|  | @ -682,7 +682,7 @@ export default { | |||
|       return this.$apollo | ||||
|         .mutate(mutationPayload) | ||||
|         .then((res) => this.onUploadDesignDone(res)) | ||||
|         .catch(() => this.onUploadDesignError()); | ||||
|         .catch((error) => this.onUploadDesignError(error)); | ||||
|     }, | ||||
|     afterUploadDesign(store, { data: { designManagementUpload } }) { | ||||
|       updateStoreAfterUploadDesign(store, designManagementUpload, this.designCollectionQueryBody); | ||||
|  | @ -702,7 +702,8 @@ export default { | |||
|       // reset state | ||||
|       this.resetFilesToBeSaved(); | ||||
|     }, | ||||
|     onUploadDesignError() { | ||||
|     onUploadDesignError(error) { | ||||
|       Sentry.captureException(error); | ||||
|       this.resetFilesToBeSaved(); | ||||
|       this.designUploadError = UPLOAD_DESIGN_ERROR_MESSAGE; | ||||
|     }, | ||||
|  |  | |||
|  | @ -275,7 +275,7 @@ export default { | |||
| <template> | ||||
|   <li class="tree-item !gl-px-0 !gl-py-2"> | ||||
|     <div class="gl-flex gl-items-start"> | ||||
|       <div v-if="hasIndirectChildren" class="gl-mr-2 gl-h-7 gl-w-5"> | ||||
|       <div v-if="hasIndirectChildren" class="gl-mr-4 gl-h-7 gl-w-5"> | ||||
|         <gl-button | ||||
|           v-if="shouldExpandChildren" | ||||
|           v-gl-tooltip.hover | ||||
|  | @ -285,7 +285,7 @@ export default { | |||
|           category="tertiary" | ||||
|           size="small" | ||||
|           :loading="isLoadingChildren && !fetchNextPageInProgress" | ||||
|           class="!gl-px-0 !gl-py-3" | ||||
|           class="!gl-py-3" | ||||
|           data-testid="expand-child" | ||||
|           @click.stop="toggleItem" | ||||
|         /> | ||||
|  |  | |||
|  | @ -344,3 +344,12 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem; | |||
|     overflow-y: scroll !important; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .design-list-item { | ||||
|   height: 160px; | ||||
|   text-decoration: none; | ||||
| 
 | ||||
|   &:hover { | ||||
|     border-color: var(--gray-400, $gray-400); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -3,6 +3,8 @@ | |||
| class Admin::HealthCheckController < Admin::ApplicationController | ||||
|   feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned | ||||
| 
 | ||||
|   authorize! :read_admin_health_check, only: [:show] | ||||
| 
 | ||||
|   def show | ||||
|     @errors = HealthCheck::Utils.process_checks(checks) | ||||
|   end | ||||
|  |  | |||
|  | @ -37,7 +37,8 @@ module MergeRequests | |||
|         committers: [merge_request_diff: [:merge_request_diff_commits]], | ||||
|         suggested_reviewers: [:predictions], | ||||
|         diff_stats: [latest_merge_request_diff: [:merge_request_diff_commits]], | ||||
|         source_branch_exists: [:source_project, { source_project: [:route] }] | ||||
|         source_branch_exists: [:source_project, { source_project: [:route] }], | ||||
|         squash_read_only: { target_project: :project_setting } | ||||
|       } | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -266,6 +266,10 @@ module Types | |||
|       HEREDOC | ||||
|     field :squash_on_merge, GraphQL::Types::Boolean, null: false, method: :squash_on_merge?, | ||||
|       description: 'Indicates if the merge request will be squashed when merged.' | ||||
|     field :squash_read_only, GraphQL::Types::Boolean, | ||||
|       null: false, | ||||
|       description: 'Indicates if `squashReadOnly` is enabled.', | ||||
|       method: :squash_readonly? | ||||
|     field :timelogs, Types::TimelogType.connection_type, null: false, | ||||
|       description: 'Timelogs on the merge request.' | ||||
| 
 | ||||
|  |  | |||
|  | @ -254,6 +254,17 @@ class WorkItem < Issue | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def lazy_user_notes | ||||
|     BatchLoader.for(id).batch(default_value: []) do |work_item_ids, loader| | ||||
|       issue_user_notes = ::Note.where(noteable_type: 'Issue', noteable_id: work_item_ids) | ||||
|       issue_user_notes.each_batch do |batch| | ||||
|         batch.user.each do |note| | ||||
|           loader.call(note.noteable_id) { |notes| notes << note } | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   override :parent_link_confidentiality | ||||
|  |  | |||
|  | @ -5,18 +5,18 @@ | |||
|   description: page_description, | ||||
|   options: { data: { event_tracking_load: 'true', event_tracking: 'view_admin_health_check_pageload' } } ) | ||||
| 
 | ||||
| .form-group | ||||
| - if can?(current_user, :admin_all_resources) | ||||
|   .form-group | ||||
|     = label_tag :health_check_access_token, s_('HealthCheck|Access token') | ||||
|     .gl-flex.gl-gap-3 | ||||
|       = text_field_tag :health_check_access_token, Gitlab::CurrentSettings.health_check_access_token, class: "form-control gl-w-28", readonly: true, data: { testid: 'health_check_token' } | ||||
|       = render Pajamas::ButtonComponent.new(href: reset_health_check_token_admin_application_settings_path, method: :put, button_options: { data: { confirm: _('Are you sure you want to reset the health check token?') } }) do | ||||
|         = _("Reset token") | ||||
| 
 | ||||
| 
 | ||||
| - help_url = help_page_path('administration/monitoring/health_check.md') | ||||
| - help_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_url } | ||||
| %p.gl-mb-1= html_escape(s_('HealthCheck|Health information can be retrieved from the following endpoints. More information is available in the %{linkStart}health check documentation%{linkEnd}.')) % { linkStart: help_start, linkEnd: '</a>'.html_safe } | ||||
| %ul.gl-mb-6 | ||||
|   - help_url = help_page_path('administration/monitoring/health_check.md') | ||||
|   - help_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_url } | ||||
|   %p.gl-mb-1= html_escape(s_('HealthCheck|Health information can be retrieved from the following endpoints. More information is available in the %{linkStart}health check documentation%{linkEnd}.')) % { linkStart: help_start, linkEnd: '</a>'.html_safe } | ||||
|   %ul.gl-mb-6 | ||||
|     %li.gl-mb-1 | ||||
|       %code= readiness_url(token: Gitlab::CurrentSettings.health_check_access_token) | ||||
|     %li.gl-mb-1 | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
|           = link_to user_path(user), class: 'gl-text-default', data: { event_tracking: 'click_search_result', event_label: @scope, event_value: position } do | ||||
|             .gl-inline-block.gl-font-bold= simple_search_highlight_and_truncate(user.name, @search_term) | ||||
|             = user_status(user) | ||||
|             %div{ class: '!gl-text-left' }= simple_search_highlight_and_truncate(user.to_reference, @search_term) | ||||
|             %div{ class: '!gl-text-left gl-text-subtle' }= simple_search_highlight_and_truncate(user.to_reference, @search_term) | ||||
|   %td.gl-text-right{ data: { label: _('Activity') } } | ||||
|     %div | ||||
|       %span.gl-font-bold= _('User created:') | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| --- | ||||
| table_name: ai_active_context_migrations | ||||
| classes: | ||||
| - Ai::ActiveContext::Migration | ||||
| feature_categories: | ||||
| - global_search | ||||
| description: Tracks the state and progress of AI active context migrations. | ||||
|   Each migration is identified by a unique version timestamp and is used for computing or updating collections in ai context abstraction layer. | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/181238 | ||||
| milestone: '17.10' | ||||
| gitlab_schema: gitlab_main_cell | ||||
| exempt_from_sharding: true | ||||
| table_size: small | ||||
|  | @ -0,0 +1,43 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class AddActiveContextMigrations < Gitlab::Database::Migration[2.2] | ||||
|   milestone '17.10' | ||||
| 
 | ||||
|   FAILED_STATE_ENUM = 255 | ||||
|   INDEX_NAME = 'index_ai_active_context_migrations_on_connection_and_status' | ||||
|   UNIQUE_INDEX_NAME = 'index_ai_active_context_migrations_on_connection_and_version' | ||||
|   CONSTRAINT_NAME = 'c_ai_active_context_migrations_on_retries_left' | ||||
|   VERSION_CONSTRAINT_NAME = 'c_ai_active_context_migrations_version_format' | ||||
|   CONSTRAINT_QUERY = <<~SQL | ||||
|     (retries_left > 0) OR (retries_left = 0 AND status = #{FAILED_STATE_ENUM}) | ||||
|   SQL | ||||
| 
 | ||||
|   def change | ||||
|     create_table :ai_active_context_migrations do |t| | ||||
|       # Fixed size columns (8 bytes) | ||||
|       t.references :connection, index: false, | ||||
|         foreign_key: { to_table: :ai_active_context_connections, on_delete: :cascade }, null: false | ||||
|       t.datetime_with_timezone :started_at | ||||
|       t.datetime_with_timezone :completed_at | ||||
|       t.timestamps_with_timezone | ||||
| 
 | ||||
|       # Fixed size columns (4 bytes) | ||||
|       t.integer :status, limit: 2, null: false, default: 0 | ||||
|       t.integer :retries_left, limit: 2, null: false | ||||
| 
 | ||||
|       # Variable size columns | ||||
|       t.text :version, null: false, limit: 255 | ||||
|       t.jsonb :metadata, null: false, default: {} | ||||
|       t.text :error_message, limit: 1024 | ||||
| 
 | ||||
|       t.index [:connection_id, :status], name: INDEX_NAME | ||||
|       t.index [:connection_id, :version], unique: true, name: UNIQUE_INDEX_NAME | ||||
| 
 | ||||
|       # Check constraint ensures retries_left is only non-zero for non-failed migrations | ||||
|       t.check_constraint CONSTRAINT_QUERY, name: CONSTRAINT_NAME | ||||
| 
 | ||||
|       # Check constraint ensures version is a 14-digit timestamp format (YYYYMMDDHHMMSS) | ||||
|       t.check_constraint "version ~ '^[0-9]{14}$'", name: VERSION_CONSTRAINT_NAME | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| ff25d3c1a1f25af9447a6463ff867ac4d29741f440a5b2bc91d757cbb9aa77f6 | ||||
|  | @ -7157,6 +7157,33 @@ CREATE SEQUENCE ai_active_context_connections_id_seq | |||
| 
 | ||||
| ALTER SEQUENCE ai_active_context_connections_id_seq OWNED BY ai_active_context_connections.id; | ||||
| 
 | ||||
| CREATE TABLE ai_active_context_migrations ( | ||||
|     id bigint NOT NULL, | ||||
|     connection_id bigint NOT NULL, | ||||
|     started_at timestamp with time zone, | ||||
|     completed_at timestamp with time zone, | ||||
|     created_at timestamp with time zone NOT NULL, | ||||
|     updated_at timestamp with time zone NOT NULL, | ||||
|     status smallint DEFAULT 0 NOT NULL, | ||||
|     retries_left smallint NOT NULL, | ||||
|     version text NOT NULL, | ||||
|     metadata jsonb DEFAULT '{}'::jsonb NOT NULL, | ||||
|     error_message text, | ||||
|     CONSTRAINT c_ai_active_context_migrations_on_retries_left CHECK (((retries_left > 0) OR ((retries_left = 0) AND (status = 255)))), | ||||
|     CONSTRAINT c_ai_active_context_migrations_version_format CHECK ((version ~ '^[0-9]{14}$'::text)), | ||||
|     CONSTRAINT check_184ab3430e CHECK ((char_length(error_message) <= 1024)), | ||||
|     CONSTRAINT check_b2e8a34818 CHECK ((char_length(version) <= 255)) | ||||
| ); | ||||
| 
 | ||||
| CREATE SEQUENCE ai_active_context_migrations_id_seq | ||||
|     START WITH 1 | ||||
|     INCREMENT BY 1 | ||||
|     NO MINVALUE | ||||
|     NO MAXVALUE | ||||
|     CACHE 1; | ||||
| 
 | ||||
| ALTER SEQUENCE ai_active_context_migrations_id_seq OWNED BY ai_active_context_migrations.id; | ||||
| 
 | ||||
| CREATE TABLE ai_agent_version_attachments ( | ||||
|     id bigint NOT NULL, | ||||
|     created_at timestamp with time zone NOT NULL, | ||||
|  | @ -24978,6 +25005,8 @@ ALTER TABLE ONLY ai_active_context_collections ALTER COLUMN id SET DEFAULT nextv | |||
| 
 | ||||
| ALTER TABLE ONLY ai_active_context_connections ALTER COLUMN id SET DEFAULT nextval('ai_active_context_connections_id_seq'::regclass); | ||||
| 
 | ||||
| ALTER TABLE ONLY ai_active_context_migrations ALTER COLUMN id SET DEFAULT nextval('ai_active_context_migrations_id_seq'::regclass); | ||||
| 
 | ||||
| ALTER TABLE ONLY ai_agent_version_attachments ALTER COLUMN id SET DEFAULT nextval('ai_agent_version_attachments_id_seq'::regclass); | ||||
| 
 | ||||
| ALTER TABLE ONLY ai_agent_versions ALTER COLUMN id SET DEFAULT nextval('ai_agent_versions_id_seq'::regclass); | ||||
|  | @ -26919,6 +26948,9 @@ ALTER TABLE ONLY ai_active_context_collections | |||
| ALTER TABLE ONLY ai_active_context_connections | ||||
|     ADD CONSTRAINT ai_active_context_connections_pkey PRIMARY KEY (id); | ||||
| 
 | ||||
| ALTER TABLE ONLY ai_active_context_migrations | ||||
|     ADD CONSTRAINT ai_active_context_migrations_pkey PRIMARY KEY (id); | ||||
| 
 | ||||
| ALTER TABLE ONLY ai_agent_version_attachments | ||||
|     ADD CONSTRAINT ai_agent_version_attachments_pkey PRIMARY KEY (id); | ||||
| 
 | ||||
|  | @ -31465,6 +31497,10 @@ CREATE INDEX index_agent_user_access_on_project_id ON agent_user_access_project_ | |||
| 
 | ||||
| CREATE UNIQUE INDEX index_ai_active_context_connections_on_name ON ai_active_context_connections USING btree (name); | ||||
| 
 | ||||
| CREATE INDEX index_ai_active_context_migrations_on_connection_and_status ON ai_active_context_migrations USING btree (connection_id, status); | ||||
| 
 | ||||
| CREATE UNIQUE INDEX index_ai_active_context_migrations_on_connection_and_version ON ai_active_context_migrations USING btree (connection_id, version); | ||||
| 
 | ||||
| CREATE INDEX index_ai_agent_version_attachments_on_ai_agent_version_id ON ai_agent_version_attachments USING btree (ai_agent_version_id); | ||||
| 
 | ||||
| CREATE INDEX index_ai_agent_version_attachments_on_ai_vectorizable_file_id ON ai_agent_version_attachments USING btree (ai_vectorizable_file_id); | ||||
|  | @ -41320,6 +41356,9 @@ ALTER TABLE ONLY ml_candidate_metadata | |||
| ALTER TABLE ONLY merge_request_merge_schedules | ||||
|     ADD CONSTRAINT fk_rails_5294434bc3 FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE; | ||||
| 
 | ||||
| ALTER TABLE ONLY ai_active_context_migrations | ||||
|     ADD CONSTRAINT fk_rails_52b6529477 FOREIGN KEY (connection_id) REFERENCES ai_active_context_connections(id) ON DELETE CASCADE; | ||||
| 
 | ||||
| ALTER TABLE ONLY elastic_group_index_statuses | ||||
|     ADD CONSTRAINT fk_rails_52b9969b12 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE; | ||||
| 
 | ||||
|  |  | |||
|  | @ -29305,6 +29305,7 @@ Defines which user roles, users, or groups can merge into a protected branch. | |||
| | <a id="mergerequestsourceprojectid"></a>`sourceProjectId` | [`Int`](#int) | ID of the merge request source project. | | ||||
| | <a id="mergerequestsquash"></a>`squash` | [`Boolean!`](#boolean) | Indicates if the merge request is set to be squashed when merged. [Project settings](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html#configure-squash-options-for-a-project) may override this value. Use `squash_on_merge` instead to take project squash options into account. | | ||||
| | <a id="mergerequestsquashonmerge"></a>`squashOnMerge` | [`Boolean!`](#boolean) | Indicates if the merge request will be squashed when merged. | | ||||
| | <a id="mergerequestsquashreadonly"></a>`squashReadOnly` | [`Boolean!`](#boolean) | Indicates if `squashReadOnly` is enabled. | | ||||
| | <a id="mergerequeststate"></a>`state` | [`MergeRequestState!`](#mergerequeststate) | State of the merge request. | | ||||
| | <a id="mergerequestsubscribed"></a>`subscribed` | [`Boolean!`](#boolean) | Indicates if the currently logged in user is subscribed to this merge request. | | ||||
| | <a id="mergerequestsuggestedreviewers"></a>`suggestedReviewers` | [`SuggestedReviewersType`](#suggestedreviewerstype) | Suggested reviewers for merge request. | | ||||
|  |  | |||
|  | @ -51,7 +51,7 @@ GET /projects/:id/resource_groups/:key | |||
| | Attribute | Type    | Required | Description         | | ||||
| |-----------|---------|----------|---------------------| | ||||
| | `id`      | integer/string     | yes      | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) | | ||||
| | `key`     | string  | yes      | The key of the resource group | | ||||
| | `key`     | string  | yes      | The URL-encoded key of the resource group. For example, use `resource%5Fa` instead of `resource_a`. | | ||||
| 
 | ||||
| ```shell | ||||
| curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/resource_groups/production" | ||||
|  | @ -78,7 +78,7 @@ GET /projects/:id/resource_groups/:key/upcoming_jobs | |||
| | Attribute | Type    | Required | Description         | | ||||
| |-----------|---------|----------|---------------------| | ||||
| | `id`      | integer/string     | yes      | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths) | | ||||
| | `key`     | string  | yes      | The key of the resource group | | ||||
| | `key`     | string  | yes      | The URL-encoded key of the resource group. For example, use `resource%5Fa` instead of `resource_a`. | | ||||
| 
 | ||||
| ```shell | ||||
| curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/50/resource_groups/production/upcoming_jobs" | ||||
|  | @ -177,7 +177,7 @@ PUT /projects/:id/resource_groups/:key | |||
| | Attribute       | Type    | Required                          | Description                      | | ||||
| | --------------- | ------- | --------------------------------- | -------------------------------  | | ||||
| | `id`            | integer/string | yes                        | The ID or [URL-encoded path of the project](rest/_index.md#namespaced-paths)            | | ||||
| | `key`           | string  | yes                               | The key of the resource group | | ||||
| | `key`           | string  | yes                               |The URL-encoded key of the resource group. For example, use `resource%5Fa` instead of `resource_a`. | | ||||
| | `process_mode`  | string  | no                                | The process mode of the resource group. One of `unordered`, `oldest_first` or `newest_first`. Read [process modes](../ci/resource_groups/_index.md#process-modes) for more information. | | ||||
| 
 | ||||
| ```shell | ||||
|  |  | |||
|  | @ -122,23 +122,25 @@ delete_resources: on_stop    # Resources are removed when environment is stopped | |||
| Environment templates support limited variable substitution. | ||||
| The following variables are available: | ||||
| 
 | ||||
| | Category | Variable | Description | | ||||
| |----------|----------|-------------| | ||||
| | Agent | `{{ .agent.id }}` | The agent identifier. | | ||||
| | Agent | `{{ .agent.name }}` | The agent name. | | ||||
| | Agent | `{{ .agent.url }}` | The agent URL. | | ||||
| | Environment | `{{ .environment.name }}` | The environment name. | | ||||
| | Environment | `{{ .environment.slug }}` | The environment slug. | | ||||
| | Environment | `{{ .environment.url }}` | The environment URL. | | ||||
| | Environment | `{{ .environment.tier }}` | The environment tier. | | ||||
| | Project | `{{ .project.id }}` | The project identifier. | | ||||
| | Project | `{{ .project.slug }}` | The project slug. | | ||||
| | Project | `{{ .project.path }}` | The project path. | | ||||
| | Project | `{{ .project.url }}` | The project URL. | | ||||
| | CI Pipeline | `{{ .ci_pipeline.id }}` | The pipeline identifier. | | ||||
| | CI Job | `{{ .ci_job.id }}` | The CI/CD job identifier. | | ||||
| | User | `{{ .user.id }}` | The user identifier. | | ||||
| | User | `{{ .user.username }}` | The username. | | ||||
| | Category       | Variable                      | Description               | Type    | Default value when not set | | ||||
| |----------------|-------------------------------|---------------------------|---------|----------------------------| | ||||
| | Agent          | `{{ .agent.id }}`             | The agent ID.             | Integer | N/A                       | | ||||
| | Agent          | `{{ .agent.name }}`           | The agent name.           | String  | N/A                       | | ||||
| | Agent          | `{{ .agent.url }}`            | The agent URL.            | String  | N/A                       | | ||||
| | Environment    | `{{ .environment.id }}`       | The environment ID.       | Integer | N/A                       | | ||||
| | Environment    | `{{ .environment.name }}`     | The environment name.     | String  | N/A                       | | ||||
| | Environment    | `{{ .environment.slug }}`     | The environment slug.     | String  | N/A                       | | ||||
| | Environment    | `{{ .environment.url }}`      | The environment URL.      | String  | Empty string               | | ||||
| | Environment    | `{{ .environment.page_url }}` | The environment page URL. | String  | N/A                       | | ||||
| | Environment    | `{{ .environment.tier }}`     | The environment tier.     | String  | N/A                       | | ||||
| | Project        | `{{ .project.id }}`           | The project ID.           | Integer | N/A                       | | ||||
| | Project        | `{{ .project.slug }}`         | The project slug.         | String  | N/A                       | | ||||
| | Project        | `{{ .project.path }}`         | The project path.         | String  | N/A                       | | ||||
| | Project        | `{{ .project.url }}`          | The project URL.          | String  | N/A                       | | ||||
| | CI/CD Pipeline | `{{ .ci_pipeline.id }}`       | The pipeline ID.          | Integer | Zero                       | | ||||
| | CI/CD Job      | `{{ .ci_job.id }}`            | The CI/CD job ID.         | Integer | Zero                       | | ||||
| | User           | `{{ .user.id }}`              | The user ID.              | Integer | N/A                       | | ||||
| | User           | `{{ .user.username }}`        | The username.             | String  | N/A                       | | ||||
| 
 | ||||
| All variables should be referenced using the double curly brace syntax, for example: `{{ .project.id }}`. | ||||
| See [`text/template`](https://pkg.go.dev/text/template) documentation for more information on the templating system used. | ||||
|  | @ -151,16 +153,18 @@ The following labels are defined on every resource created by GitLab. The values | |||
| 
 | ||||
| - `agent.gitlab.com/id-<agent_id>: ""` | ||||
| - `agent.gitlab.com/project_id-<project_id>: ""` | ||||
| - `agent.gitlab.com/env-<kubernetes_namespace>: ""` | ||||
| - `agent.gitlab.com/env-<gitlab_environment_slug>-<project_id>-<agent_id>: ""` | ||||
| - `agent.gitlab.com/environment_slug-<gitlab_environment_slug>: ""` | ||||
| 
 | ||||
| On every resource created by GitLab, an `agent.gitlab.com/env-<kubernetes_namespace>` annotation is defined. The value of the annotation is a JSON object with the following keys: | ||||
| On every resource created by GitLab, an `agent.gitlab.com/env-<gitlab_environment_slug>-<project_id>-<agent_id>` annotation is defined. | ||||
| The value of the annotation is a JSON object with the following keys: | ||||
| 
 | ||||
| | Key | Description                                      | | ||||
| |-----|-------------| | ||||
| |-----|--------------------------------------------------| | ||||
| | `environment_id` | The GitLab environment ID.                       | | ||||
| | `environment_name` | The GitLab environment name.                     | | ||||
| | `environment_slug` | The GitLab environment slug.                     | | ||||
| | `environment_url` | The link to the environment. Optional.           | | ||||
| | `environment_page_url` | The link to the GitLab environment page.         | | ||||
| | `environment_tier` | The GitLab environment deployment tier.          | | ||||
| | `agent_id` | The agent ID.                                    | | ||||
|  |  | |||
|  | @ -240,7 +240,7 @@ for a package uploaded to the [GitLab package registry](../packages/package_regi | |||
| varies by format: | ||||
| 
 | ||||
| | Package type           | GitLab.com                         | | ||||
| |---------------------------|------------| | ||||
| |------------------------|------------------------------------| | ||||
| | Conan                  | 5 GB                               | | ||||
| | Generic                | 5 GB                               | | ||||
| | Helm                   | 5 MB                               | | ||||
|  | @ -249,7 +249,7 @@ varies by format: | |||
| | NuGet                  | 5 GB                               | | ||||
| | PyPI                   | 5 GB                               | | ||||
| | Terraform              | 1 GB                               | | ||||
| | Machine learning model    | 10 GB      | | ||||
| | Machine learning model | 10 GB (uploads are capped at 5 GB) | | ||||
| 
 | ||||
| ## Account and limit settings | ||||
| 
 | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ title: Value stream analytics | |||
| 
 | ||||
| {{< /details >}} | ||||
| 
 | ||||
| Value stream analytics measures the time it takes to go from an idea to production. | ||||
| Value stream analytics measures the time it takes to go from an idea to production by tracking merge request or issue events. | ||||
| 
 | ||||
| A **value stream** is the entire work process that delivers value to customers. For example, | ||||
| the [DevOps lifecycle](https://about.gitlab.com/stages-devops-lifecycle/) is a value stream that starts | ||||
|  | @ -30,7 +30,7 @@ Value stream analytics helps businesses: | |||
| 
 | ||||
| - Visualize their end-to-end DevSecOps workstreams. | ||||
| - Identify and solve inefficiencies. | ||||
| - Optimize their workstreams to deliver more value, faster. | ||||
| - Optimize their workstreams to deliver more value, faster (for example, [reducing merge request review time](https://about.gitlab.com/blog/2025/02/20/how-we-reduced-mr-review-time-with-value-stream-management/)). | ||||
| 
 | ||||
| Value stream analytics is available for projects and groups. | ||||
| 
 | ||||
|  | @ -119,6 +119,8 @@ These events play a key role in the duration calculation, which is calculated by | |||
| 
 | ||||
| To learn what start and end events can be paired, see [Validating start and end events](../../../development/value_stream_analytics.md#validating-start-and-end-events). | ||||
| 
 | ||||
| You can share your ideas or feedback about stage events in [issue 520962](https://gitlab.com/gitlab-org/gitlab/-/issues/520962). | ||||
| 
 | ||||
| ### How value stream analytics aggregates data | ||||
| 
 | ||||
| {{< details >}} | ||||
|  |  | |||
| Before Width: | Height: | Size: 22 KiB | 
|  | @ -100,7 +100,7 @@ first in the list of changed files. To copy a merge request link that shows your | |||
| 1. Below the merge request title, select **Changes**. | ||||
| 1. Find the file you want to show first. Right-click the name of the file to copy the link to it. | ||||
| 1. When you visit that link, your chosen file is shown at the top of the list. The file browser | ||||
|    shows a link icon ({{< icon name="link" >}}) next to the file name: | ||||
|    shows a link icon ({{< icon name="link" >}}) next to the filename: | ||||
| 
 | ||||
|     | ||||
| 
 | ||||
|  | @ -230,13 +230,13 @@ To change how a merge request shows changed lines: | |||
| 
 | ||||
|    {{< tab title="Inline changes" >}} | ||||
| 
 | ||||
|     | ||||
|     | ||||
| 
 | ||||
|    {{< /tab >}} | ||||
| 
 | ||||
|    {{< tab title="Side-by-side changes" >}} | ||||
| 
 | ||||
|     | ||||
|     | ||||
| 
 | ||||
|    {{< /tab >}} | ||||
| 
 | ||||
|  | @ -303,7 +303,7 @@ When reviewing code changes, you can hide inline comments: | |||
| 1. Select **Code > Merge requests** and find your merge request. | ||||
| 1. Below the title, select **Changes**. | ||||
| 1. Scroll to the file that contains the comments you want to hide. | ||||
| 1. Scroll to the line the comment is attached to, and select **Collapse** ({{< icon name="collapse" >}}): | ||||
| 1. Scroll to the line the comment is attached to. In the gutter margin, select **Collapse** ({{< icon name="collapse" >}}): | ||||
|     | ||||
| 
 | ||||
| To expand inline comments and show them again: | ||||
|  | @ -312,8 +312,8 @@ To expand inline comments and show them again: | |||
| 1. Select **Code > Merge requests** and find your merge request. | ||||
| 1. Below the title, select **Changes**. | ||||
| 1. Scroll to the file that contains the collapsed comments you want to show. | ||||
| 1. Scroll to the line the comment is attached to, and select the user avatar: | ||||
|     | ||||
| 1. Scroll to the line the comment is attached to. In the gutter margin, select the user avatar: | ||||
|     | ||||
| 
 | ||||
| ## Ignore whitespace changes | ||||
| 
 | ||||
|  | @ -326,12 +326,12 @@ a merge request. You can choose to hide or show whitespace changes: | |||
| 1. Before the list of changed files, select **Preferences** ({{< icon name="preferences" >}}). | ||||
| 1. Select or clear **Show whitespace changes**: | ||||
| 
 | ||||
|     | ||||
|     | ||||
| 
 | ||||
| ## Mark files as viewed | ||||
| 
 | ||||
| When reviewing a merge request with many files multiple times, you can ignore files | ||||
| you've already reviewed. To hide files that haven't changed since your last review: | ||||
| you've already reviewed. To hide files that haven't changed after your last review: | ||||
| 
 | ||||
| 1. On the left sidebar, select **Search or go to** and find your project. | ||||
| 1. Select **Code > Merge requests** and find your merge request. | ||||
|  |  | |||
| Before Width: | Height: | Size: 8.3 KiB | 
| After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 9.1 KiB | 
| After Width: | Height: | Size: 18 KiB | 
| Before Width: | Height: | Size: 13 KiB | 
| After Width: | Height: | Size: 7.6 KiB | 
| Before Width: | Height: | Size: 26 KiB | 
| After Width: | Height: | Size: 11 KiB | 
|  | @ -142,9 +142,7 @@ In that case, if _any_ of these protected tags have a setting like | |||
| **Allowed to create**, then `production-stable` also inherit this setting. | ||||
| 
 | ||||
| If you select a protected tag's name, GitLab displays a list of | ||||
| all matching tags: | ||||
| 
 | ||||
|  | ||||
| all matching tags. | ||||
| 
 | ||||
| ## Prevent tag creation with the same name as branches | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,7 +14,9 @@ module API | |||
| 
 | ||||
|         preload_repository_cache(projects_relation) | ||||
| 
 | ||||
|         Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_relation, options[:current_user]).execute if options[:current_user] | ||||
|         if options[:current_user] | ||||
|           Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects_relation, options[:current_user]).execute | ||||
|         end | ||||
| 
 | ||||
|         preload_member_roles(projects_relation, options[:current_user]) if options[:current_user] | ||||
|         preload_groups(projects_relation) if options[:with] == Entities::Project | ||||
|  | @ -24,7 +26,7 @@ module API | |||
| 
 | ||||
|       # This is overridden by the specific Entity class to | ||||
|       # preload assocations that it needs | ||||
|       def preload_relation(projects_relation, options = {}) | ||||
|       def preload_relation(projects_relation, _options = {}) | ||||
|         projects_relation | ||||
|       end | ||||
| 
 | ||||
|  |  | |||
|  | @ -53837,6 +53837,9 @@ msgstr "" | |||
| msgid "Selected commits" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Selected database" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Selected for all items." | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,32 +1,190 @@ | |||
| import * as utils from '~/blob/utils'; | ||||
| import { TEST_HOST } from 'helpers/test_constants'; | ||||
| import setWindowLocation from 'helpers/set_window_location_helper'; | ||||
| 
 | ||||
| describe('Blob utilities', () => { | ||||
|   beforeEach(() => { | ||||
|     jest.clearAllMocks(); | ||||
|     document.body.innerHTML = ''; | ||||
|   }); | ||||
| 
 | ||||
|   describe('getPageParamValue', () => { | ||||
|     it('returns empty string if no perPage parameter is provided', () => { | ||||
|       const pageParamValue = utils.getPageParamValue(5); | ||||
|       expect(pageParamValue).toEqual(''); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns empty string if page is equal 1', () => { | ||||
|       const pageParamValue = utils.getPageParamValue(1000, 1000); | ||||
|       expect(pageParamValue).toEqual(''); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns correct page parameter value', () => { | ||||
|       const pageParamValue = utils.getPageParamValue(1001, 1000); | ||||
|       expect(pageParamValue).toEqual(2); | ||||
|     }); | ||||
| 
 | ||||
|     it('accepts strings as a parameter and returns correct result', () => { | ||||
|       const pageParamValue = utils.getPageParamValue('1001', '1000'); | ||||
|       expect(pageParamValue).toEqual(2); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getPageSearchString', () => { | ||||
|     it('returns empty search string if page parameter is empty value', () => { | ||||
|       const path = utils.getPageSearchString('/blamePath', ''); | ||||
|       expect(path).toEqual(''); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns correct search string if value is provided', () => { | ||||
|       const searchString = utils.getPageSearchString('/blamePath', 3); | ||||
|       const searchString = utils.getPageSearchString('http://project/blamePath', 3); | ||||
|       expect(searchString).toEqual('?page=3'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('moveToFilePermalink', () => { | ||||
|     const initialTitle = 'Title · Test'; | ||||
|     let windowHistorySpy; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       windowHistorySpy = jest.spyOn(window.history, 'pushState'); | ||||
|       setWindowLocation(TEST_HOST); | ||||
|       document.title = initialTitle; | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|       jest.restoreAllMocks(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should do nothing when permalink element does not exist', () => { | ||||
|       utils.moveToFilePermalink(); | ||||
| 
 | ||||
|       expect(windowHistorySpy).not.toHaveBeenCalled(); | ||||
|       expect(document.title).toMatch(initialTitle); | ||||
|     }); | ||||
| 
 | ||||
|     it('should do nothing when permalink element exists but has no href', () => { | ||||
|       document.body.innerHTML = ` | ||||
|         <div class="js-data-file-blob-permalink-url"> | ||||
|           <a data-testid="permalink"></a> | ||||
|         </div> | ||||
|       `;
 | ||||
| 
 | ||||
|       utils.moveToFilePermalink(); | ||||
| 
 | ||||
|       expect(windowHistorySpy).not.toHaveBeenCalled(); | ||||
|       expect(document.title).toMatch(initialTitle); | ||||
|     }); | ||||
| 
 | ||||
|     it('should not update history when URL is not different', () => { | ||||
|       const url = `${TEST_HOST}/test/permalink`; | ||||
|       document.body.innerHTML = ` | ||||
|          <a data-testid="permalink" class="js-data-file-blob-permalink-url" href="${url}"></a> | ||||
|       `;
 | ||||
|       setWindowLocation(url); | ||||
| 
 | ||||
|       utils.moveToFilePermalink(); | ||||
| 
 | ||||
|       expect(windowHistorySpy).not.toHaveBeenCalled(); | ||||
|       expect(document.title).toMatch(initialTitle); | ||||
|     }); | ||||
| 
 | ||||
|     it('should update history and title when URL is different and contains SHA', () => { | ||||
|       const testSha = 'ad9be38573f9ee4c4daec22673478c2dd1d81cd8'; | ||||
|       document.body.innerHTML = ` | ||||
|          <a class="js-data-file-blob-permalink-url" data-testid="permalink" href="/test/permalink/${testSha}"></a> | ||||
|       `;
 | ||||
| 
 | ||||
|       utils.moveToFilePermalink(); | ||||
| 
 | ||||
|       expect(windowHistorySpy).toHaveBeenCalledWith({}, initialTitle, `/test/permalink/${testSha}`); | ||||
|       expect(document.title).toMatch(`Title · ${testSha}`); | ||||
|     }); | ||||
| 
 | ||||
|     it('should update history but not title when URL is different but contains no SHA', () => { | ||||
|       document.body.innerHTML = ` | ||||
|         <a class="js-data-file-blob-permalink-url" data-testid="permalink" href="/test/permalink"></a> | ||||
|       `;
 | ||||
| 
 | ||||
|       utils.moveToFilePermalink(); | ||||
| 
 | ||||
|       expect(windowHistorySpy).toHaveBeenCalledWith({}, initialTitle, `/test/permalink`); | ||||
|       expect(document.title).toMatch(initialTitle); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('shortcircuitPermalinkButton', () => { | ||||
|     let permalinkElement; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       permalinkElement = document.createElement('a'); | ||||
|       permalinkElement.dataset.testid = 'permalink'; | ||||
|       permalinkElement.className = 'js-data-file-blob-permalink-url'; | ||||
| 
 | ||||
|       document.body.appendChild(permalinkElement); | ||||
|     }); | ||||
| 
 | ||||
|     afterEach(() => { | ||||
|       document.body.innerHTML = ''; | ||||
|       jest.clearAllMocks(); | ||||
|     }); | ||||
| 
 | ||||
|     it('attaches click event listener to permalink element', () => { | ||||
|       const addEventListenerSpy = jest.spyOn(permalinkElement, 'addEventListener'); | ||||
| 
 | ||||
|       utils.shortcircuitPermalinkButton(); | ||||
| 
 | ||||
|       expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function)); | ||||
|     }); | ||||
| 
 | ||||
|     it('does nothing if permalink element is not found', () => { | ||||
|       document.body.innerHTML = ''; | ||||
| 
 | ||||
|       expect(() => { | ||||
|         utils.shortcircuitPermalinkButton(); | ||||
|       }).not.toThrow(); | ||||
|     }); | ||||
| 
 | ||||
|     describe('click handling', () => { | ||||
|       beforeEach(() => { | ||||
|         utils.shortcircuitPermalinkButton(); | ||||
|       }); | ||||
| 
 | ||||
|       afterEach(() => { | ||||
|         jest.restoreAllMocks(); | ||||
|       }); | ||||
| 
 | ||||
|       it('prevents default and calls moveToFilePermalink for normal click', () => { | ||||
|         const clickEvent = new MouseEvent('click'); | ||||
|         const preventDefaultSpy = jest.spyOn(clickEvent, 'preventDefault'); | ||||
|         const querySelectorSpy = jest.spyOn(document, 'querySelector'); | ||||
| 
 | ||||
|         permalinkElement.dispatchEvent(clickEvent); | ||||
| 
 | ||||
|         expect(preventDefaultSpy).toHaveBeenCalled(); | ||||
|         // Because we can't mock moveToFilePermalink, we are asserting it's being called by
 | ||||
|         // asserting that the first line inside the method is being executed:
 | ||||
|         expect(querySelectorSpy).toHaveBeenCalledWith('.js-data-file-blob-permalink-url'); | ||||
|       }); | ||||
| 
 | ||||
|       it.each([ | ||||
|         ['ctrl', { ctrlKey: true }], | ||||
|         ['meta', { metaKey: true }], | ||||
|         ['shift', { shiftKey: true }], | ||||
|       ])('does not prevent default or call moveToFilePermalink for %s click', (_, modifiers) => { | ||||
|         const clickEvent = new MouseEvent('click', { | ||||
|           ...modifiers, | ||||
|         }); | ||||
|         const preventDefaultSpy = jest.spyOn(clickEvent, 'preventDefault'); | ||||
|         const querySelectorSpy = jest.spyOn(document, 'querySelector'); | ||||
| 
 | ||||
|         permalinkElement.dispatchEvent(clickEvent); | ||||
| 
 | ||||
|         expect(preventDefaultSpy).not.toHaveBeenCalled(); | ||||
|         // Because we can't mock moveToFilePermalink, we are asserting it's being called by
 | ||||
|         // asserting that the first line inside the method is being executed:
 | ||||
|         expect(querySelectorSpy).not.toHaveBeenCalled(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -67,9 +67,7 @@ describe('LabelActions', () => { | |||
|     expect(deleteItem).toMatchObject({ | ||||
|       text: 'Delete', | ||||
|       action: expect.any(Function), | ||||
|       extraAttrs: { | ||||
|         class: '!gl-text-red-500', | ||||
|       }, | ||||
|       variant: 'danger', | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ RSpec.describe GitlabSchema.types['MergeRequest'], feature_category: :code_revie | |||
|       milestone assignees reviewers participants subscribed labels discussion_locked time_estimate | ||||
|       total_time_spent human_time_estimate human_total_time_spent reference author merged_at closed_at | ||||
|       commit_count current_user_todos conflicts auto_merge_enabled approved_by source_branch_protected | ||||
|       squash_on_merge available_auto_merge_strategies | ||||
|       squash_on_merge squash_read_only available_auto_merge_strategies | ||||
|       has_ci mergeable commits committers commits_without_merge_commits squash security_auto_fix default_squash_commit_message | ||||
|       auto_merge_strategy merge_user award_emoji prepared_at codequality_reports_comparer supports_lock_on_merge | ||||
|       mergeability_checks merge_after | ||||
|  |  | |||
|  | @ -963,4 +963,29 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do | |||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#lazy_user_notes' do | ||||
|     it 'returns user notes lazily with 1 SQL query' do | ||||
|       project_work_item = create(:work_item) | ||||
|       create(:note_on_work_item, project: project_work_item.project, noteable: project_work_item) | ||||
|       group_work_item = create(:work_item, :group_level) | ||||
|       create(:note_on_work_item, namespace: project_work_item.namespace, noteable: group_work_item) | ||||
|       work_items = [project_work_item, group_work_item] | ||||
| 
 | ||||
|       recorder = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) do | ||||
|         work_items.each(&:lazy_user_notes) | ||||
|         work_items.each do |work_item| | ||||
|           expect(work_item.lazy_user_notes).to match_array(work_item.notes) | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       # from batch loader for lazy_user_notes | ||||
|       # 3 queries to find notes using each_batch | ||||
|       # when run in EE context, an additional query is made for the issues | ||||
|       # from spec itself | ||||
|       # 2 queries to load the notes to verify lazy_user_notes returns the right notes | ||||
|       expected_count = Gitlab.ee? ? 6 : 5 | ||||
|       expect(recorder.count).to eq(expected_count) | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -696,7 +696,6 @@ | |||
| - './ee/spec/helpers/path_locks_helper_spec.rb' | ||||
| - './ee/spec/helpers/preferences_helper_spec.rb' | ||||
| - './ee/spec/helpers/prevent_forking_helper_spec.rb' | ||||
| - './ee/spec/helpers/projects_helper_spec.rb' | ||||
| - './ee/spec/helpers/projects/on_demand_scans_helper_spec.rb' | ||||
| - './ee/spec/helpers/projects/project_members_helper_spec.rb' | ||||
| - './ee/spec/helpers/projects/security/dast_profiles_helper_spec.rb' | ||||
|  |  | |||