diff options
38 files changed, 427 insertions, 225 deletions
diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml index 41f85c492d9..482736ad1ac 100644 --- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml +++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml @@ -125,6 +125,7 @@ download-fast-quarantine-report: gdk-qa-smoke: extends: - .gdk-qa-base + - .gitlab-qa-report variables: QA_SCENARIO: Test::Instance::Smoke QA_RUN_TYPE: gdk-qa-smoke @@ -152,6 +153,7 @@ gdk-qa-smoke-with-load-balancer: gdk-qa-reliable: extends: - .gdk-qa-base + - .gitlab-qa-report - .parallel variables: QA_SCENARIO: Test::Instance::Blocking diff --git a/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql new file mode 100644 index 00000000000..ed318ef1b8d --- /dev/null +++ b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users.query.graphql @@ -0,0 +1,12 @@ +#import "../fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" + +query projectAutocompleteUsersSearch($search: String!, $fullPath: ID!) { + workspace: project(fullPath: $fullPath) { + id + users: autocompleteUsers(search: $search) { + ...User + ...UserAvailability + } + } +} diff --git a/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql new file mode 100644 index 00000000000..8155451fb7c --- /dev/null +++ b/app/assets/javascripts/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql @@ -0,0 +1,19 @@ +#import "../fragments/user.fragment.graphql" +#import "~/graphql_shared/fragments/user_availability.fragment.graphql" + +query projectAutocompleteUsersSearchWithMRPermissions( + $search: String! + $fullPath: ID! + $mergeRequestId: MergeRequestID! +) { + workspace: project(fullPath: $fullPath) { + id + users: autocompleteUsers(search: $search) { + ...User + ...UserAvailability + mergeRequestInteraction(id: $mergeRequestId) { + canMerge + } + } + } +} diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js index 0f82182c6e2..752d0315227 100644 --- a/app/assets/javascripts/sidebar/constants.js +++ b/app/assets/javascripts/sidebar/constants.js @@ -1,8 +1,8 @@ import { invert } from 'lodash'; import { s__, __, sprintf } from '~/locale'; import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutation.graphql'; -import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql'; -import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; +import userAutocompleteQuery from '~/graphql_shared/queries/project_autocomplete_users.query.graphql'; +import userAutocompleteWithMRPermissionsQuery from '~/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql'; import { TYPE_ALERT, TYPE_EPIC, @@ -106,10 +106,10 @@ export const participantsQueries = { export const userSearchQueries = { [TYPE_ISSUE]: { - query: userSearchQuery, + query: userAutocompleteQuery, }, [TYPE_MERGE_REQUEST]: { - query: userSearchWithMRPermissionsQuery, + query: userAutocompleteWithMRPermissionsQuery, }, }; diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue index 4879baced0d..1e79d2cdcd7 100644 --- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue +++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue @@ -130,11 +130,11 @@ export default { }, update(data) { return ( - data.workspace?.users?.nodes - .filter((x) => x?.user) - .map((node) => ({ - ...node.user, - canMerge: node.mergeRequestInteraction?.canMerge || false, + data.workspace?.users + .filter((user) => user) + .map((user) => ({ + ...user, + canMerge: user.mergeRequestInteraction?.canMerge || false, })) || [] ); }, diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 0b54de469f8..a896ca5cabc 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -14,8 +14,6 @@ module Groups # TODO - add a policy check here https://gitlab.com/gitlab-org/gitlab/-/issues/353082 raise DestroyError, "You can't delete this group because you're blocked." if current_user.blocked? - group.prepare_for_destroy - group.projects.includes(:project_feature).each do |project| # Execute the destruction of the models immediately to ensure atomic cleanup. success = ::Projects::DestroyService.new(project, current_user).execute diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb index 3c0ba1514e5..a0e1167836b 100644 --- a/app/services/users/destroy_service.rb +++ b/app/services/users/destroy_service.rb @@ -59,9 +59,6 @@ module Users Groups::DestroyService.new(group, current_user).execute end - namespace = user.namespace - namespace.prepare_for_destroy - user.personal_projects.each do |project| success = ::Projects::DestroyService.new(project, current_user).execute raise DestroyError, "Project #{project.id} can't be deleted" unless success diff --git a/config/feature_flags/development/limited_access_modal.yml b/config/feature_flags/development/limited_access_modal.yml new file mode 100644 index 00000000000..b567b9ce0d4 --- /dev/null +++ b/config/feature_flags/development/limited_access_modal.yml @@ -0,0 +1,8 @@ +--- +name: limited_access_modal +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129790 +rollout_issue_url: +milestone: '16.4' +type: development +group: group::billing and subscription management +default_enabled: false diff --git a/config/feature_flags/development/scan_execution_bot_users.yml b/config/feature_flags/development/scan_execution_bot_users.yml deleted file mode 100644 index ca06e666e67..00000000000 --- a/config/feature_flags/development/scan_execution_bot_users.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: scan_execution_bot_users -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/118089 -rollout_issue_url: -milestone: '16.0' -type: development -group: group::security policies -default_enabled: true diff --git a/db/post_migrate/20230825085648_ensure_backfill_for_ci_stages_pipeline_id_is_finished.rb b/db/post_migrate/20230825085648_ensure_backfill_for_ci_stages_pipeline_id_is_finished.rb new file mode 100644 index 00000000000..3dabd352a1b --- /dev/null +++ b/db/post_migrate/20230825085648_ensure_backfill_for_ci_stages_pipeline_id_is_finished.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class EnsureBackfillForCiStagesPipelineIdIsFinished < Gitlab::Database::Migration[2.1] + include Gitlab::Database::MigrationHelpers::ConvertToBigint + + restrict_gitlab_migration gitlab_schema: :gitlab_ci + disable_ddl_transaction! + + TABLE_NAME = :ci_stages + + def up + ensure_batched_background_migration_is_finished( + job_class_name: 'CopyColumnUsingBackgroundMigrationJob', + table_name: TABLE_NAME, + column_name: 'id', + job_arguments: [['pipeline_id'], ['pipeline_id_convert_to_bigint']] + ) + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20230825085719_create_async_index_for_ci_stages_pipeline_id.rb b/db/post_migrate/20230825085719_create_async_index_for_ci_stages_pipeline_id.rb new file mode 100644 index 00000000000..a517d96815a --- /dev/null +++ b/db/post_migrate/20230825085719_create_async_index_for_ci_stages_pipeline_id.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class CreateAsyncIndexForCiStagesPipelineId < Gitlab::Database::Migration[2.1] + TABLE_NAME = :ci_stages + INDEXES = { + 'index_ci_stages_on_pipeline_id_convert_to_bigint_and_name' => [ + [:pipeline_id_convert_to_bigint, :name], { unique: true } + ], + 'index_ci_stages_on_pipeline_id_convert_to_bigint' => [ + [:pipeline_id_convert_to_bigint], {} + ], + 'index_ci_stages_on_pipeline_id_convert_to_bigint_and_id' => [ + [:pipeline_id_convert_to_bigint, :id], { where: 'status = ANY (ARRAY[0, 1, 2, 8, 9, 10])' } + ], + 'index_ci_stages_on_pipeline_id_convert_to_bigint_and_position' => [ + [:pipeline_id_convert_to_bigint, :position], {} + ] + } + + def up + INDEXES.each do |index_name, (columns, options)| + prepare_async_index TABLE_NAME, columns, name: index_name, **options + end + end + + def down + INDEXES.each do |index_name, (columns, options)| + unprepare_async_index TABLE_NAME, columns, name: index_name, **options + end + end +end diff --git a/db/schema_migrations/20230825085648 b/db/schema_migrations/20230825085648 new file mode 100644 index 00000000000..a6b1d8e1be1 --- /dev/null +++ b/db/schema_migrations/20230825085648 @@ -0,0 +1 @@ +5e003d34a36320c53852ece7d0373ce99e7fc21b08f8edc5f5320256d4b3b3a2
\ No newline at end of file diff --git a/db/schema_migrations/20230825085719 b/db/schema_migrations/20230825085719 new file mode 100644 index 00000000000..cf785c0a170 --- /dev/null +++ b/db/schema_migrations/20230825085719 @@ -0,0 +1 @@ +4b7b8711a29a8a26ff9af42d73b95eb52b1791569771b1c34f6f51000059b10d
\ No newline at end of file diff --git a/doc/.vale/gitlab/BadgeCapitalization.yml b/doc/.vale/gitlab/BadgeCapitalization.yml index 6e77c3fe822..a44bcbc0a7d 100644 --- a/doc/.vale/gitlab/BadgeCapitalization.yml +++ b/doc/.vale/gitlab/BadgeCapitalization.yml @@ -10,5 +10,5 @@ link: https://docs.gitlab.com/ee/development/documentation/styleguide/index.html level: error scope: raw raw: - - '(?!\*\*\((FREE|PREMIUM|ULTIMATE)( (SELF|SAAS))?\)\*\*)' - - '(?i)\*\*\((free|premium|ultimate)( (self|saas))?\)\*\*' + - '(?!\*\*\((FREE|PREMIUM|ULTIMATE)( (SELF|SAAS|ALL) (BETA|EXPERIMENT))?\)\*\*)' + - '(?i)\*\*\((free|premium|ultimate)( (self|saas|all) (beta|experiment))?\)\*\*' diff --git a/doc/.vale/gitlab/Uppercase.yml b/doc/.vale/gitlab/Uppercase.yml index 4730184b950..b13ebe2c0a8 100644 --- a/doc/.vale/gitlab/Uppercase.yml +++ b/doc/.vale/gitlab/Uppercase.yml @@ -16,6 +16,7 @@ second: '(?:\b[A-Z][a-z]+ )+\(([A-Z]{3,5})\)' exceptions: - ACL - AJAX + - ALL - AMI - ANSI - APAC @@ -27,6 +28,7 @@ exceptions: - ASG - AST - AWS + - BETA - BMP - BSD - CAS diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 90d27749b0c..246610ecd4c 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -1152,7 +1152,7 @@ Input type: `AiActionInput` | <a id="mutationaiactiongeneratecommitmessage"></a>`generateCommitMessage` | [`AiGenerateCommitMessageInput`](#aigeneratecommitmessageinput) | Input for generate_commit_message AI action. | | <a id="mutationaiactiongeneratedescription"></a>`generateDescription` | [`AiGenerateDescriptionInput`](#aigeneratedescriptioninput) | Input for generate_description AI action. | | <a id="mutationaiactiongeneratetestfile"></a>`generateTestFile` | [`GenerateTestFileInput`](#generatetestfileinput) | Input for generate_test_file AI action. | -| <a id="mutationaiactionmarkupformat"></a>`markupFormat` | [`MarkupFormat`](#markupformat) | Indicates the response format. | +| <a id="mutationaiactionmarkupformat"></a>`markupFormat` **{warning-solid}** | [`MarkupFormat`](#markupformat) | **Deprecated:** Moved to contentHtml attribute. Deprecated in 16.4. | | <a id="mutationaiactionsummarizecomments"></a>`summarizeComments` | [`AiSummarizeCommentsInput`](#aisummarizecommentsinput) | Input for summarize_comments AI action. | | <a id="mutationaiactionsummarizereview"></a>`summarizeReview` | [`AiSummarizeReviewInput`](#aisummarizereviewinput) | Input for summarize_review AI action. | | <a id="mutationaiactiontanukibot"></a>`tanukiBot` | [`AiTanukiBotInput`](#aitanukibotinput) | Input for tanuki_bot AI action. | @@ -12539,6 +12539,7 @@ Information about a connected Agent. | Name | Type | Description | | ---- | ---- | ----------- | | <a id="aicachedmessagetypecontent"></a>`content` | [`String`](#string) | Content of the message. Can be null for failed responses. | +| <a id="aicachedmessagetypecontenthtml"></a>`contentHtml` | [`String`](#string) | HTML content of the message. Can be null for failed responses. | | <a id="aicachedmessagetypeerrors"></a>`errors` | [`[String!]!`](#string) | Errors that occurred while asynchronously fetching an AI (assistant) response. | | <a id="aicachedmessagetypeid"></a>`id` | [`ID`](#id) | UUID of the message. | | <a id="aicachedmessagetyperequestid"></a>`requestId` | [`ID`](#id) | UUID of the original request message. | @@ -12566,6 +12567,7 @@ Information about a connected Agent. | <a id="airesponseerrors"></a>`errors` | [`[String!]`](#string) | Errors return by AI API as response. | | <a id="airesponserequestid"></a>`requestId` | [`String`](#string) | ID of the original request. | | <a id="airesponseresponsebody"></a>`responseBody` | [`String`](#string) | Response body from AI API. | +| <a id="airesponseresponsebodyhtml"></a>`responseBodyHtml` | [`String`](#string) | Response body HTML. | | <a id="airesponserole"></a>`role` | [`AiCachedMessageRole!`](#aicachedmessagerole) | Message role. | | <a id="airesponsetimestamp"></a>`timestamp` | [`Time!`](#time) | Message timestamp. | | <a id="airesponsetype"></a>`type` | [`String`](#string) | Message type. | @@ -27160,7 +27162,6 @@ List markup formats. | Value | Description | | ----- | ----------- | | <a id="markupformathtml"></a>`HTML` | HTML format. | -| <a id="markupformatmarkdown"></a>`MARKDOWN` | Markdown format. | | <a id="markupformatraw"></a>`RAW` | Raw format. | ### `MeasurementIdentifier` diff --git a/doc/ci/runners/saas/macos_saas_runner.md b/doc/ci/runners/saas/macos_saas_runner.md index 036bf187bb9..982db707525 100644 --- a/doc/ci/runners/saas/macos_saas_runner.md +++ b/doc/ci/runners/saas/macos_saas_runner.md @@ -56,7 +56,7 @@ GitLab provides `stable` and `latest` macOS images that follow different update By definition, the `latest` images are always Beta. A `latest` image is not available. -### Image release process** +### Image release process When Apple releases a new macOS version, GitLab releases both `stable` and `latest` images based on the OS in the next release. Both images are Beta. diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md index 9d53ddae60d..e0042277b99 100644 --- a/doc/security/two_factor_authentication.md +++ b/doc/security/two_factor_authentication.md @@ -8,9 +8,14 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Enforce two-factor authentication **(FREE ALL)** [Two-factor authentication (2FA)](../user/profile/account/two_factor_authentication.md) -provides an additional level of security to your users' GitLab account. When enabled, -users are prompted for a code generated by an application in addition to supplying -their username and password to sign in. +is an authentication method that requires the user to provide two different factors +to prove their identity: + +- Username and password. +- A second authentication method, such as a code generated by an application. + +2FA makes it harder for an unauthorized person to access an account because +they would need both factors. NOTE: If you are [using and enforcing SSO](../user/group/saml_sso/index.md#sso-enforcement), you might already be enforcing 2FA on the identity provider (IDP) side. Enforcing 2FA on GitLab as well might be unnecessary. diff --git a/doc/user/application_security/policies/scan-execution-policies.md b/doc/user/application_security/policies/scan-execution-policies.md index dc59d615b65..59aa880ff9f 100644 --- a/doc/user/application_security/policies/scan-execution-policies.md +++ b/doc/user/application_security/policies/scan-execution-policies.md @@ -113,10 +113,10 @@ This rule enforces the defined actions whenever the pipeline runs for a selected > - The `branch_type` field was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/404774) in GitLab 16.1 [with a flag](../../../administration/feature_flags.md) named `security_policies_branch_type`. Disabled by default. > - The `branch_type` field was [enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/413062) in GitLab 16.2. -> - The security policy bot users were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/394958) in GitLab 16.3 [with flags](../../../administration/feature_flags.md) named `scan_execution_group_bot_users` and `scan_execution_bot_users`. Enabled by default. +> - The security policy bot users were [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/394958) in GitLab 16.3 [with flag](../../../administration/feature_flags.md) named `scan_execution_group_bot_users`. Enabled by default. FLAG: -On self-managed GitLab, security policy bot users are available. To hide the feature, an administrator can [disable the feature flags](../../../administration/feature_flags.md) named `scan_execution_group_bot_users` and `scan_execution_bot_users`. +On self-managed GitLab, security policy bot users are available. To hide the feature, an administrator can [disable the feature flag](../../../administration/feature_flags.md) named `scan_execution_group_bot_users`. On GitLab.com, this feature is available. This rule schedules a scan pipeline, enforcing the defined actions on the schedule defined in the `cadence` field. A scheduled pipeline does not run other jobs defined in the project's `.gitlab-ci.yml` file. When a project is linked to a security policy project, a security policy bot is created in the project and will become the author of any scheduled pipelines. diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md index 7c8b21ab3d6..e1d4fc48f2f 100644 --- a/doc/user/product_analytics/index.md +++ b/doc/user/product_analytics/index.md @@ -24,6 +24,7 @@ This feature is not ready for production use. This page is a work in progress, and we're updating the information as we add more features. For more information, see the [group direction page](https://about.gitlab.com/direction/analytics/product-analytics/). +To leave feedback about Product Analytics bugs or functionality, please comment in [issue 391970](https://gitlab.com/gitlab-org/gitlab/-/issues/391970) or open a new issue with the label `group::product analytics`. ## How product analytics works diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md index fb8022a1048..d8e4fce41ef 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/index.md @@ -113,7 +113,7 @@ Subdomains (`subdomain.example.com`) require: Whether it's a user or a project website, the DNS record should point to your Pages domain (`namespace.gitlab.io`), -without any path (for example, `/project-slug`). +without any path. ![DNS `CNAME` record pointing to GitLab.com project](img/dns_cname_record_example.png) diff --git a/doc/user/project/pages/getting_started_part_one.md b/doc/user/project/pages/getting_started_part_one.md index e3750152a29..ec511ee0a5f 100644 --- a/doc/user/project/pages/getting_started_part_one.md +++ b/doc/user/project/pages/getting_started_part_one.md @@ -25,14 +25,13 @@ For GitLab self-managed instances, replace `example.io` with your instance's Pages domain. For GitLab.com, Pages domains are `*.gitlab.io`. -| Type of GitLab Pages | The path of the project created in GitLab | Website URL | +| Type of GitLab Pages | Example path of a project in GitLab | Website URL | | -------------------- | ------------ | ----------- | | User pages | `username/username.example.io` | `http(s)://username.example.io` | | Group pages | `acmecorp/acmecorp.example.io` | `http(s)://acmecorp.example.io` | | Project pages owned by a user | `username/my-website` | `http(s)://username.example.io/my-website` | | Project pages owned by a group | `acmecorp/webshop` | `http(s)://acmecorp.example.io/webshop`| | Project pages owned by a subgroup | `acmecorp/documentation/product-manual` | `http(s)://acmecorp.example.io/documentation/product-manual`| -| Project pages owned by a subgroup | `group-path/subgroup-slug/project-slug` | `http(s)://group-path.example.io/subgroup-slug/project-slug`| WARNING: There are some known [limitations](introduction.md#subdomains-of-subdomains) diff --git a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb index 13799b8b9ff..503e05f12e9 100644 --- a/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb +++ b/gems/gitlab-schema-validation/lib/gitlab/schema/validation/inconsistency.rb @@ -36,6 +36,17 @@ module Gitlab Diffy::Diff.new(structure_sql_statement, database_statement) end + def to_h + { + type: type, + object_type: object_type, + table_name: table_name, + object_name: object_name, + structure_sql_statement: structure_sql_statement, + database_statement: database_statement + } + end + def display <<~MSG #{'-' * 54} diff --git a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb index 268bb4556e3..300383d5909 100644 --- a/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb +++ b/gems/gitlab-schema-validation/spec/lib/gitlab/schema/validation/inconsistency_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Schema::Validation::Inconsistency do +RSpec.describe Gitlab::Schema::Validation::Inconsistency, feature_category: :database do let(:validator) { Gitlab::Schema::Validation::Validators::DifferentDefinitionIndexes } let(:database_statement) { 'CREATE INDEX index_name ON public.achievements USING btree (namespace_id)' } @@ -44,6 +44,23 @@ RSpec.describe Gitlab::Schema::Validation::Inconsistency do end end + describe '#to_h' do + let(:result) do + { + database_statement: inconsistency.database_statement, + object_name: inconsistency.object_name, + object_type: inconsistency.object_type, + structure_sql_statement: inconsistency.structure_sql_statement, + table_name: inconsistency.table_name, + type: inconsistency.type + } + end + + it 'returns the to_h of the validator' do + expect(inconsistency.to_h).to eq(result) + end + end + describe '#table_name' do it 'returns the table name' do expect(inconsistency.table_name).to eq('achievements') diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index 45466a1894c..5c2337cdde4 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -76,7 +76,7 @@ module API requires :base_sha, type: String, desc: 'Base commit SHA in the source branch' requires :start_sha, type: String, desc: 'SHA referencing commit in target branch' requires :head_sha, type: String, desc: 'SHA referencing HEAD of this merge request' - requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image) + requires :position_type, type: String, desc: 'Type of the position reference', values: %w(text image file) optional :new_path, type: String, desc: 'File path after change' optional :new_line, type: Integer, desc: 'Line number after change' optional :old_path, type: String, desc: 'File path before change' diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb index 7ccfd195108..17c784c4d54 100644 --- a/lib/gitlab/ci/components/instance_path.rb +++ b/lib/gitlab/ci/components/instance_path.rb @@ -13,12 +13,13 @@ module Gitlab address.include?('@') && address.start_with?(Settings.gitlab_ci['component_fqdn']) end - attr_reader :host + attr_reader :host, :project_file_path def initialize(address:, content_filename:) @full_path, @version = address.to_s.split('@', 2) @content_filename = content_filename @host = Settings.gitlab_ci['component_fqdn'] + @project_file_path = nil end def fetch_content!(current_user:) @@ -27,7 +28,7 @@ module Gitlab raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project) - templates_dir_path_content || content(sha, custom_dir_template_file_path) + content(simple_template_path) || content(complex_template_path) || content(legacy_template_path) end def project @@ -43,12 +44,6 @@ module Gitlab end strong_memoize_attr :sha - def project_file_path - return unless project - - custom_dir_template_file_path - end - private attr_reader :version, :path @@ -58,7 +53,7 @@ module Gitlab end def component_path - instance_path.delete_prefix(project.full_path) + instance_path.delete_prefix(project.full_path).delete_prefix('/') end strong_memoize_attr :component_path @@ -81,31 +76,33 @@ module Gitlab project.releases.latest&.sha end - def custom_dir_template_file_path - File.join(component_path, @content_filename).delete_prefix('/') - end + # A simple template consists of a single file + def simple_template_path + # Extract this line and move to fetch_content once we remove legacy fetching + return unless templates_dir_exists? && component_path.index('/').nil? - def templates_dir_file_path - File.join(TEMPLATES_DIR, "#{component_path}.yml") + @project_file_path = File.join(TEMPLATES_DIR, "#{component_path}.yml") end + # A complex template is directory-based and may consist of multiple files. # Given a path like "my-org/sub-group/the-project/templates/component" - # returns "templates/component/template.yml" - def templates_dir_template_file_path - File.join(TEMPLATES_DIR, component_path, @content_filename) - end + # returns the entry point path: "templates/component/template.yml". + def complex_template_path + # Extract this line and move to fetch_content once we remove legacy fetching + return unless templates_dir_exists? && component_path.index('/').nil? - def templates_dir_exists? - project.repository.tree.trees.map(&:name).include?(TEMPLATES_DIR) + @project_file_path = File.join(TEMPLATES_DIR, component_path, @content_filename) end - def templates_dir_path_content - return unless templates_dir_exists? + def legacy_template_path + @project_file_path = File.join(component_path, @content_filename).delete_prefix('/') + end - content(sha, templates_dir_file_path) || content(sha, templates_dir_template_file_path) + def templates_dir_exists? + project.repository.tree.trees.map(&:name).include?(TEMPLATES_DIR) end - def content(sha, path) + def content(path) project.repository.blob_data_at(sha, path) end end diff --git a/lib/gitlab/utils/markdown.rb b/lib/gitlab/utils/markdown.rb index c041953e7c8..68e9206b4d5 100644 --- a/lib/gitlab/utils/markdown.rb +++ b/lib/gitlab/utils/markdown.rb @@ -4,7 +4,7 @@ module Gitlab module Utils module Markdown PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u.freeze - PRODUCT_SUFFIX = /\s*\**\((core|starter|premium|ultimate|free|bronze|silver|gold)(\s+(all|only|self|saas))?\)\**/.freeze + PRODUCT_SUFFIX = /\s*\**\((premium|ultimate|free|beta|experiment)(\s+(all|self|saas))?(\s+(beta|experiment))?\)\**/.freeze def string_to_anchor(string) string diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6303647a4d2..7ca4705cf81 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -45791,6 +45791,18 @@ msgstr "" msgid "SubscriptionBanner|Upload new license" msgstr "" +msgid "SubscriptionMangement|If you'd like to add more seats, upgrade your plan, or purchase additional products, contact your GitLab sales representative." +msgstr "" + +msgid "SubscriptionMangement|This is a custom subscription managed by the GitLab Sales team" +msgstr "" + +msgid "SubscriptionMangement|To make changes to a read-only subscription or purchase additional products, contact your GitLab Partner." +msgstr "" + +msgid "SubscriptionMangement|Your subscription is in read-only mode" +msgstr "" + msgid "SubscriptionTable|Add seats" msgstr "" diff --git a/package.json b/package.json index 006be9fabed..637bb99f021 100644 --- a/package.json +++ b/package.json @@ -251,7 +251,7 @@ "cheerio": "^1.0.0-rc.9", "commander": "^2.20.3", "custom-jquery-matchers": "^2.1.0", - "eslint": "8.47.0", + "eslint": "8.48.0", "eslint-import-resolver-jest": "3.0.2", "eslint-import-resolver-webpack": "0.13.7", "eslint-plugin-import": "^2.28.1", diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index b0db17875c5..7d044c4aa92 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -71,6 +71,12 @@ FactoryBot.define do association :author, factory: :user end + trait :user_namespace_level do + project { nil } + association :namespace, factory: :user_namespace + association :author, factory: :user + end + trait :issue do association :work_item_type, :default, :issue end diff --git a/spec/frontend/sidebar/mock_data.js b/spec/frontend/sidebar/mock_data.js index 05a7f504fd4..9d8392ad5f0 100644 --- a/spec/frontend/sidebar/mock_data.js +++ b/spec/frontend/sidebar/mock_data.js @@ -414,6 +414,33 @@ export const searchQueryResponse = { }, }; +export const searchAutocompleteQueryResponse = { + data: { + workspace: { + __typename: 'Project', + id: '', + users: [ + { + id: '1', + avatarUrl: '/avatar', + name: 'root', + username: 'root', + webUrl: 'root', + status: null, + }, + { + id: '2', + avatarUrl: '/avatar2', + name: 'rookie', + username: 'rookie', + webUrl: 'rookie', + status: null, + }, + ], + }, + }, +}; + export const updateIssueAssigneesMutationResponse = { data: { issuableSetAssignees: { @@ -545,6 +572,29 @@ export const searchResponseOnMR = { }, }; +export const searchAutocompleteResponseOnMR = { + data: { + workspace: { + __typename: 'Project', + id: '1', + users: [ + { + ...mockUser1, + mergeRequestInteraction: { + canMerge: true, + }, + }, + { + ...mockUser2, + mergeRequestInteraction: { + canMerge: false, + }, + }, + ], + }, + }, +}; + export const projectMembersResponse = { data: { workspace: { @@ -585,6 +635,36 @@ export const projectMembersResponse = { }, }; +export const projectAutocompleteMembersResponse = { + data: { + workspace: { + id: '1', + __typename: 'Project', + users: [ + // Remove nulls https://gitlab.com/gitlab-org/gitlab/-/issues/329750 + null, + null, + // Remove duplicated entry https://gitlab.com/gitlab-org/gitlab/-/issues/327822 + mockUser1, + mockUser1, + mockUser2, + { + __typename: 'UserCore', + id: 'gid://gitlab/User/2', + avatarUrl: + 'https://www.gravatar.com/avatar/a95e5b71488f4b9d69ce5ff58bfd28d6?s=80\u0026d=identicon', + name: 'Jacki Kub', + username: 'francina.skiles', + webUrl: '/franc', + status: { + availability: 'BUSY', + }, + }, + ], + }, + }, +}; + export const groupMembersResponse = { data: { workspace: { diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js index 8c7657da8bc..119b892392f 100644 --- a/spec/frontend/vue_shared/components/user_select_spec.js +++ b/spec/frontend/vue_shared/components/user_select_spec.js @@ -5,17 +5,17 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; -import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql'; -import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql'; +import searchUsersQuery from '~/graphql_shared/queries/project_autocomplete_users.query.graphql'; +import searchUsersQueryOnMR from '~/graphql_shared/queries/project_autocomplete_users_with_mr_permissions.query.graphql'; import { TYPE_MERGE_REQUEST } from '~/issues/constants'; import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue'; import getIssueParticipantsQuery from '~/sidebar/queries/get_issue_participants.query.graphql'; import UserSelect from '~/vue_shared/components/user_select/user_select.vue'; import { - searchResponse, - searchResponseOnMR, - projectMembersResponse, + projectAutocompleteMembersResponse, + searchAutocompleteQueryResponse, + searchAutocompleteResponseOnMR, participantsQueryResponse, mockUser1, mockUser2, @@ -59,7 +59,7 @@ describe('User select dropdown', () => { const findUnassignLink = () => wrapper.findByTestId('unassign'); const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results'); - const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse); + const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectAutocompleteMembersResponse); const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse); const createComponent = ({ @@ -69,7 +69,7 @@ describe('User select dropdown', () => { } = {}) => { fakeApollo = createMockApollo([ [searchUsersQuery, searchQueryHandler], - [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)], + [searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchAutocompleteResponseOnMR)], [getIssueParticipantsQuery, participantsQueryHandler], ]); wrapper = shallowMountExtended(UserSelect, { @@ -200,7 +200,7 @@ describe('User select dropdown', () => { }); await waitForPromises(); - expect(findUnselectedParticipantByIndex(0).props('user')).toEqual(mockUser2); + expect(findUnselectedParticipantByIndex(0).props('user')).toMatchObject(mockUser2); }); it('moves issuable author on top of unassigned list after current user, if author and current user are unassigned project members', async () => { @@ -372,7 +372,9 @@ describe('User select dropdown', () => { }); it('renders a list of found users and external participants matching search term', async () => { - createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) }); + createComponent({ + searchQueryHandler: jest.fn().mockResolvedValue(searchAutocompleteQueryResponse), + }); await waitForPromises(); findSearchField().vm.$emit('input', 'ro'); @@ -382,7 +384,9 @@ describe('User select dropdown', () => { }); it('renders a list of found users only if no external participants match search term', async () => { - createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(searchResponse) }); + createComponent({ + searchQueryHandler: jest.fn().mockResolvedValue(searchAutocompleteQueryResponse), + }); await waitForPromises(); findSearchField().vm.$emit('input', 'roo'); @@ -392,8 +396,8 @@ describe('User select dropdown', () => { }); it('shows a message about no matches if search returned an empty list', async () => { - const responseCopy = cloneDeep(searchResponse); - responseCopy.data.workspace.users.nodes = []; + const responseCopy = cloneDeep(searchAutocompleteQueryResponse); + responseCopy.data.workspace.users = []; createComponent({ searchQueryHandler: jest.fn().mockResolvedValue(responseCopy), diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 007c90d458e..97843781891 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -15,23 +15,24 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline describe 'FQDN path' do let(:version) { 'master' } + let(:project_path) { project.full_path } + let(:address) { "acme.com/#{project_path}/secret-detection@#{version}" } context 'when the project repository contains a templates directory' do - let_it_be(:existing_project) do + let_it_be(:project) do create( :project, :custom_repo, files: { - 'templates/file.yml' => 'image: alpine_1', - 'templates/dir/template.yml' => 'image: alpine_2' + 'templates/secret-detection.yml' => 'image: alpine_1', + 'templates/dast/template.yml' => 'image: alpine_2', + 'templates/dast/another-template.yml' => 'image: alpine_3', + 'templates/dast/another-folder/template.yml' => 'image: alpine_4' } ) end - let(:project_path) { existing_project.full_path } - let(:address) { "acme.com/#{project_path}/file@#{version}" } - before do - existing_project.add_developer(user) + project.add_developer(user) end context 'when user does not have permissions' do @@ -41,64 +42,50 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end end - context 'when templates directory is top level' do - it 'fetches the content' do + context 'when the component is simple (single file template)' do + it 'fetches the component content', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('templates/secret-detection.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end + end - it 'provides the expected attributes', :aggregate_failures do + context 'when the component is complex (directory-based template)' do + let(:address) { "acme.com/#{project_path}/dast@#{version}" } + + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('file/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('templates/dast/template.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end - context 'when file name is `template.yml`' do - let(:address) { "acme.com/#{project_path}/dir@#{version}" } + context 'when there is an invalid nested component folder' do + let(:address) { "acme.com/#{project_path}/dast/another-folder@#{version}" } - it 'fetches the content' do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') - end - - it 'provides the expected attributes', :aggregate_failures do - expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('dir/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + it 'returns nil' do + expect(path.fetch_content!(current_user: user)).to be_nil end end - end - context 'when the project is nested under a subgroup' do - let_it_be(:existing_group) { create(:group, :nested) } - let_it_be(:existing_project) do - create( - :project, :custom_repo, - files: { - 'templates/file.yml' => 'image: alpine_1' - }, - group: existing_group - ) - end - - it 'fetches the content' do - expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') - end + context 'when there is an invalid nested component path' do + let(:address) { "acme.com/#{project_path}/dast/another-template@#{version}" } - it 'provides the expected attributes', :aggregate_failures do - expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('file/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + it 'returns nil' do + expect(path.fetch_content!(current_user: user)).to be_nil + end end end - context 'when fetching the latest version' do - let_it_be(:existing_project) do + context 'when fetching the latest version of a component' do + let_it_be(:project) do create( :project, :custom_repo, files: { - 'templates/file.yml' => 'image: alpine_1' + 'templates/secret-detection.yml' => 'image: alpine_1' } ) end @@ -106,30 +93,27 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:version) { '~latest' } let(:latest_sha) do - existing_project.repository.find_commits_by_message('Updates image').commits.last.sha + project.repository.commit('master').id end before do - create(:release, project: existing_project, sha: existing_project.repository.root_ref_sha, + create(:release, project: project, sha: project.repository.root_ref_sha, released_at: Time.zone.now - 1.day) - existing_project.repository.update_file( - user, 'templates/file.yml', 'image: alpine_2', - message: 'Updates image', branch_name: existing_project.default_branch + project.repository.update_file( + user, 'templates/secret-detection.yml', 'image: alpine_2', + message: 'Updates image', branch_name: project.default_branch ) - create(:release, project: existing_project, sha: latest_sha, + create(:release, project: project, sha: latest_sha, released_at: Time.zone.now) end - it 'fetches the content' do + it 'fetches the component content', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('file/template.yml') - expect(path.project).to eq(existing_project) + expect(path.project_file_path).to eq('templates/secret-detection.yml') + expect(path.project).to eq(project) expect(path.sha).to eq(latest_sha) end end @@ -137,31 +121,25 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline context 'when version does not exist' do let(:version) { 'non-existent' } - it 'returns nil when fetching the content' do + it 'returns nil', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to be_nil - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('file/template.yml') - expect(path.project).to eq(existing_project) + expect(path.project_file_path).to be_nil + expect(path.project).to eq(project) expect(path.sha).to be_nil end end context 'when current GitLab instance is installed on a relative URL' do - let(:address) { "acme.com/gitlab/#{project_path}/file@#{version}" } + let(:address) { "acme.com/gitlab/#{project_path}/secret-detection@#{version}" } let(:current_host) { 'acme.com/gitlab/' } - it 'fetches the content' do + it 'fetches the component content', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('file/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project_file_path).to eq('templates/secret-detection.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end end end @@ -169,10 +147,10 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline # All the following tests are for deprecated code and will be removed # in https://gitlab.com/gitlab-org/gitlab/-/issues/415855 context 'when the project does not contain a templates directory' do - let(:project_path) { existing_project.full_path } + let(:project_path) { project.full_path } let(:address) { "acme.com/#{project_path}/component@#{version}" } - let_it_be(:existing_project) do + let_it_be(:project) do create( :project, :custom_repo, files: { @@ -182,41 +160,35 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end before do - existing_project.add_developer(user) + project.add_developer(user) end - it 'fetches the content' do + it 'fetches the component content', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to eq('image: alpine') - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) expect(path.project_file_path).to eq('component/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end context 'when project path is nested under a subgroup' do - let_it_be(:existing_group) { create(:group, :nested) } - let_it_be(:existing_project) do + let_it_be(:group) { create(:group, :nested) } + let_it_be(:project) do create( :project, :custom_repo, files: { 'component/template.yml' => 'image: alpine' }, - group: existing_group + group: group ) end - it 'fetches the content' do + it 'fetches the component content', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to eq('image: alpine') - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) expect(path.project_file_path).to eq('component/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end end @@ -224,29 +196,23 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" } let(:current_host) { 'acme.com/gitlab/' } - it 'fetches the content' do + it 'fetches the component content', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to eq('image: alpine') - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) expect(path.project_file_path).to eq('component/template.yml') - expect(path.project).to eq(existing_project) - expect(path.sha).to eq(existing_project.commit('master').id) + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end end context 'when version does not exist' do let(:version) { 'non-existent' } - it 'returns nil when fetching the content' do + it 'returns nil', :aggregate_failures do expect(path.fetch_content!(current_user: user)).to be_nil - end - - it 'provides the expected attributes', :aggregate_failures do expect(path.host).to eq(current_host) - expect(path.project_file_path).to eq('component/template.yml') - expect(path.project).to eq(existing_project) + expect(path.project_file_path).to be_nil + expect(path.project).to eq(project) expect(path.sha).to be_nil end end diff --git a/spec/lib/gitlab/utils/markdown_spec.rb b/spec/lib/gitlab/utils/markdown_spec.rb index 45953c7906e..d707cf51712 100644 --- a/spec/lib/gitlab/utils/markdown_spec.rb +++ b/spec/lib/gitlab/utils/markdown_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Utils::Markdown do +RSpec.describe Gitlab::Utils::Markdown, feature_category: :gitlab_docs do let(:klass) do Class.new do include Gitlab::Utils::Markdown @@ -53,25 +53,30 @@ RSpec.describe Gitlab::Utils::Markdown do end context 'when string has a product suffix' do - %w[CORE STARTER PREMIUM ULTIMATE FREE BRONZE SILVER GOLD].each do |tier| - ['', ' ONLY', ' SELF', ' SAAS'].each do |modifier| - context "#{tier}#{modifier}" do - let(:string) { "My Header (#{tier}#{modifier})" } - - it 'ignores a product suffix' do - is_expected.to eq 'my-header' - end - - context 'with "*" around a product suffix' do - let(:string) { "My Header **(#{tier}#{modifier})**" } - - it 'ignores a product suffix' do - is_expected.to eq 'my-header' + %w[PREMIUM ULTIMATE FREE].each do |tier| + [' ALL', ' SELF', ' SAAS'].each do |modifier| + ['', ' BETA', ' EXPERIMENT'].each do |status| + context "#{tier}#{modifier}#{status}" do + context 'with "*" around a product suffix' do + let(:string) { "My Header **(#{tier}#{modifier}#{status})**" } + + it 'ignores a product suffix' do + is_expected.to eq 'my-header' + end end end end end end + %w[BETA EXPERIMENT].each do |status| + context 'with "*" around a product suffix' do + let(:string) { "My Header **(#{status})**" } + + it 'ignores a product suffix' do + is_expected.to eq 'my-header' + end + end + end end context 'when string is empty' do diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index a65dc6e0175..aebdcebbc5a 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -116,6 +116,17 @@ RSpec.describe API::Discussions, feature_category: :team_planning do it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid' it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid' + context "when position_type is file" do + it "creates a new diff note" do + position = diff_note.position.to_h.merge({ position_type: 'file' }).except(:ignore_whitespace_change) + + post api("/projects/#{parent.id}/merge_requests/#{noteable['iid']}/discussions", user), + params: { body: 'hi!', position: position } + + expect(response).to have_gitlab_http_status(:created) + end + end + context "when position is for a previous commit on the merge request" do it "returns a 400 bad request error because the line_code is old" do # SHA taken from an earlier commit listed in spec/factories/merge_requests.rb diff --git a/spec/services/ci/components/fetch_service_spec.rb b/spec/services/ci/components/fetch_service_spec.rb index 532098b3b20..21b7df19f4a 100644 --- a/spec/services/ci/components/fetch_service_spec.rb +++ b/spec/services/ci/components/fetch_service_spec.rb @@ -3,15 +3,35 @@ require 'spec_helper' RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composition do - let_it_be(:project) { create(:project, :repository, create_tag: 'v1.0') } let_it_be(:user) { create(:user) } let_it_be(:current_user) { user } let_it_be(:current_host) { Gitlab.config.gitlab.host } + let_it_be(:content) do + <<~COMPONENT + job: + script: echo + COMPONENT + end let(:service) do described_class.new(address: address, current_user: current_user) end + let_it_be(:project) do + project = create( + :project, :custom_repo, + files: { + 'template.yml' => content, + 'my-component/template.yml' => content, + 'my-dir/my-component/template.yml' => content + } + ) + + project.repository.add_tag(project.creator, 'v0.1', project.repository.commit.sha) + + project + end + before do project.add_developer(user) end @@ -22,19 +42,6 @@ RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composi shared_examples 'an external component' do shared_examples 'component address' do context 'when content exists' do - let(:sha) { project.commit(version).id } - - let(:content) do - <<~COMPONENT - job: - script: echo - COMPONENT - end - - before do - stub_project_blob(sha, component_yaml_path, content) - end - it 'returns the content' do expect(result).to be_success expect(result.payload[:content]).to eq(content) @@ -42,6 +49,8 @@ RSpec.describe Ci::Components::FetchService, feature_category: :pipeline_composi end context 'when content does not exist' do + let(:address) { "#{current_host}/#{component_path}@~version-does-not-exist" } + it 'returns an error' do expect(result).to be_error expect(result.reason).to eq(:content_not_found) diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index cabff88c83a..ebdce07d03c 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -69,10 +69,6 @@ RSpec.describe Groups::DestroyService, feature_category: :groups_and_projects do end it 'verifies that paths have been deleted' do - Gitlab::GitalyClient::NamespaceService.allow do - expect(Gitlab::GitalyClient::NamespaceService.new(project.repository_storage) - .exists?(group.path)).to be_falsey - end expect(removed_repo).not_to exist end end @@ -100,10 +96,6 @@ RSpec.describe Groups::DestroyService, feature_category: :groups_and_projects do end it 'verifies original paths and projects still exist' do - Gitlab::GitalyClient::NamespaceService.allow do - expect(Gitlab::GitalyClient::NamespaceService.new(project.repository_storage) - .exists?(group.path)).to be_truthy - end expect(removed_repo).not_to exist expect(Project.unscoped.count).to eq(1) expect(Group.unscoped.count).to eq(2) diff --git a/yarn.lock b/yarn.lock index 1ac1e64833e..c50c418fc94 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1079,10 +1079,10 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@^8.47.0": - version "8.47.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d" - integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og== +"@eslint/js@8.48.0": + version "8.48.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.48.0.tgz#642633964e217905436033a2bd08bf322849b7fb" + integrity sha512-ZSjtmelB7IJfWD2Fvb7+Z+ChTIKWq6kjda95fLcQKNS5aheVHn4IkfgRQE3sIIzTcSLwLcLZUD9UBt+V7+h+Pw== "@floating-ui/core@^1.2.6": version "1.2.6" @@ -5934,15 +5934,15 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@8.47.0: - version "8.47.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806" - integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q== +eslint@8.48.0: + version "8.48.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.48.0.tgz#bf9998ba520063907ba7bfe4c480dc8be03c2155" + integrity sha512-sb6DLeIuRXxeM1YljSe1KEx9/YYeZFQWcV8Rq9HfigmdDEugjLEVEa1ozDjL6YDjBpQHPJxJzze+alxi4T3OLg== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" "@eslint/eslintrc" "^2.1.2" - "@eslint/js" "^8.47.0" + "@eslint/js" "8.48.0" "@humanwhocodes/config-array" "^0.11.10" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" |