diff options
61 files changed, 553 insertions, 256 deletions
diff --git a/.rubocop_todo/rails/pick.yml b/.rubocop_todo/rails/pick.yml deleted file mode 100644 index 95ed3e61cb5..00000000000 --- a/.rubocop_todo/rails/pick.yml +++ /dev/null @@ -1,43 +0,0 @@ ---- -# Cop supports --auto-correct. -Rails/Pick: - # Offense count: 42 - # Temporarily disabled due to too many offenses - Enabled: false - Exclude: - - 'app/models/ci/pipeline.rb' - - 'app/models/merge_request.rb' - - 'app/models/merge_request/metrics.rb' - - 'app/models/merge_request_diff.rb' - - 'db/post_migrate/20210825193652_backfill_cadence_id_for_boards_scoped_to_iteration.rb' - - 'db/post_migrate/20220213103859_remove_integrations_type.rb' - - 'db/post_migrate/20220412143552_consume_remaining_encrypt_integration_property_jobs.rb' - - 'ee/app/models/concerns/epic_tree_sorting.rb' - - 'ee/app/models/ee/group.rb' - - 'ee/app/models/ee/namespace.rb' - - 'ee/app/models/geo/project_registry.rb' - - 'ee/lib/analytics/merge_request_metrics_calculator.rb' - - 'ee/lib/ee/gitlab/background_migration/backfill_iteration_cadence_id_for_boards.rb' - - 'ee/lib/ee/gitlab/background_migration/populate_status_column_of_security_scans.rb' - - 'ee/spec/finders/security/findings_finder_spec.rb' - - 'lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb' - - 'lib/gitlab/background_migration/backfill_ci_project_mirrors.rb' - - 'lib/gitlab/background_migration/backfill_integrations_type_new.rb' - - 'lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb' - - 'lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb' - - 'lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb' - - 'lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb' - - 'lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb' - - 'lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects.rb' - - 'lib/gitlab/background_migration/drop_invalid_security_findings.rb' - - 'lib/gitlab/background_migration/encrypt_static_object_token.rb' - - 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb' - - 'lib/gitlab/background_migration/populate_vulnerability_reads.rb' - - 'lib/gitlab/background_migration/update_timelogs_null_spent_at.rb' - - 'lib/gitlab/database/dynamic_model_helpers.rb' - - 'lib/gitlab/database/migrations/background_migration_helpers.rb' - - 'lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb' - - 'lib/gitlab/github_import/user_finder.rb' - - 'lib/gitlab/relative_positioning/item_context.rb' - - 'spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb' - - 'spec/requests/projects/cycle_analytics_events_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index df7c71c4341..27f762dda4b 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -8f39a5a9cdf3b8d544153e8297bcf639e5c626e7 +5ac494300c839eea0980d118d12ffb51f447c0ac diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index 1511f122a15..30336189d0d 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -149,8 +149,11 @@ const defaultSerializerConfig = { state.renderInline(node); state.ensureNewLine(); }), - [FootnoteReference.name]: preserveUnchanged((state, node) => { - state.write(`[^${node.attrs.identifier}]`); + [FootnoteReference.name]: preserveUnchanged({ + render: (state, node) => { + state.write(`[^${node.attrs.identifier}]`); + }, + inline: true, }), [Frontmatter.name]: (state, node) => { const { language } = node.attrs; @@ -171,7 +174,10 @@ const defaultSerializerConfig = { [HardBreak.name]: preserveUnchanged(renderHardBreak), [Heading.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.heading), [HorizontalRule.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.horizontal_rule), - [Image.name]: preserveUnchanged(renderImage), + [Image.name]: preserveUnchanged({ + render: renderImage, + inline: true, + }), [ListItem.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.list_item), [OrderedList.name]: preserveUnchanged(renderOrderedList), [Paragraph.name]: preserveUnchanged(defaultMarkdownSerializer.nodes.paragraph), diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index 4cdb690c6c3..f9ab1c1d959 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -330,12 +330,12 @@ export function renderCodeBlock(state, node) { const expandPreserveUnchangedConfig = (configOrRender) => isFunction(configOrRender) - ? { render: configOrRender, overwriteSourcePreservationStrategy: false } + ? { render: configOrRender, overwriteSourcePreservationStrategy: false, inline: false } : configOrRender; export function preserveUnchanged(configOrRender) { return (state, node, parent, index) => { - const { render, overwriteSourcePreservationStrategy } = expandPreserveUnchangedConfig( + const { render, overwriteSourcePreservationStrategy, inline } = expandPreserveUnchangedConfig( configOrRender, ); @@ -344,7 +344,10 @@ export function preserveUnchanged(configOrRender) { if (same && !overwriteSourcePreservationStrategy) { state.write(sourceMarkdown); - state.closeBlock(node); + + if (!inline) { + state.closeBlock(node); + } } else { render(state, node, parent, index, same, sourceMarkdown); } diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue index b71cfbb6112..674058bcaf0 100644 --- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue +++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue @@ -356,7 +356,7 @@ export default { data-testid="alert-member-error" > {{ $options.labels.memberErrorListText }} - <ul class="gl-pl-5"> + <ul class="gl-pl-5 gl-mb-0"> <li v-for="(error, member) in invalidMembers" :key="member"> <strong>{{ tokenName(member) }}:</strong> {{ error }} </li> diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue index b2bcb9a5906..2ddb04e1eeb 100644 --- a/app/assets/javascripts/invite_members/components/members_token_select.vue +++ b/app/assets/javascripts/invite_members/components/members_token_select.vue @@ -1,10 +1,16 @@ <script> import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@gitlab/ui'; -import { debounce } from 'lodash'; +import { debounce, isEmpty } from 'lodash'; import { __ } from '~/locale'; import { getUsers } from '~/rest_api'; import { memberName } from '../utils/member_utils'; -import { SEARCH_DELAY, USERS_FILTER_ALL, USERS_FILTER_SAML_PROVIDER_ID } from '../constants'; +import { + SEARCH_DELAY, + USERS_FILTER_ALL, + USERS_FILTER_SAML_PROVIDER_ID, + VALID_TOKEN_BACKGROUND, + INVALID_TOKEN_BACKGROUND, +} from '../constants'; export default { components: { @@ -75,6 +81,25 @@ export default { } return this.$options.defaultQueryOptions; }, + hasInvalidMembers() { + return !isEmpty(this.invalidMembers); + }, + }, + watch: { + // We might not really want this to be *reactive* since we want the "class" state to be + // tied to the specific `selectedToken` such that if the token is removed and re-added, this + // state is reset. + // See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90076#note_1027165312 + hasInvalidMembers: { + handler(updatedInvalidMembers) { + // Only update tokens if we receive invalid members + if (!updatedInvalidMembers) { + return; + } + + this.updateTokenClasses(); + }, + }, }, methods: { handleTextInput(query) { @@ -83,6 +108,12 @@ export default { this.loading = true; this.retrieveUsers(query); }, + updateTokenClasses() { + this.selectedTokens = this.selectedTokens.map((token) => ({ + ...token, + class: this.tokenClass(token), + })); + }, retrieveUsers: debounce(function debouncedRetrieveUsers() { return getUsers(this.query, this.queryOptions) .then((response) => { @@ -98,6 +129,14 @@ export default { this.loading = false; }); }, SEARCH_DELAY), + tokenClass(token) { + if (this.hasError(token)) { + return INVALID_TOKEN_BACKGROUND; + } + + // assume success for this token + return VALID_TOKEN_BACKGROUND; + }, handleInput() { this.$emit('input', this.selectedTokens); }, diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js index 6141e5e9e0b..f5187a3ef0c 100644 --- a/app/assets/javascripts/invite_members/constants.js +++ b/app/assets/javascripts/invite_members/constants.js @@ -2,6 +2,8 @@ import { s__ } from '~/locale'; export const CLOSE_TO_LIMIT_COUNT = 2; export const SEARCH_DELAY = 200; +export const VALID_TOKEN_BACKGROUND = 'gl-bg-green-100'; +export const INVALID_TOKEN_BACKGROUND = 'gl-bg-red-100'; export const INVITE_MEMBERS_FOR_TASK = { minimum_access_level: 30, name: 'invite_members_for_task', diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue index 6deb87c5dca..0c27793e7ba 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_menu.vue @@ -2,7 +2,7 @@ import { GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { produce } from 'immer'; import { s__ } from '~/locale'; -import changeWorkItemParentMutation from '../../graphql/change_work_item_parent_link.mutation.graphql'; +import updateWorkItem from '../../graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql'; import { WIDGET_TYPE_HIERARCHY } from '../../constants'; @@ -58,8 +58,10 @@ export default { }, async addChild(data) { const { data: resp } = await this.$apollo.mutate({ - mutation: changeWorkItemParentMutation, - variables: { id: this.workItemId, parentId: this.parentWorkItemId }, + mutation: updateWorkItem, + variables: { + input: { id: this.workItemId, hierarchyWidget: { parentId: this.parentWorkItemId } }, + }, update: this.toggleChildFromCache.bind(this, data), }); @@ -69,8 +71,8 @@ export default { }, async removeChild() { const { data } = await this.$apollo.mutate({ - mutation: changeWorkItemParentMutation, - variables: { id: this.workItemId, parentId: null }, + mutation: updateWorkItem, + variables: { input: { id: this.workItemId, hierarchyWidget: { parentId: null } } }, update: this.toggleChildFromCache.bind(this, null), }); diff --git a/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql b/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql deleted file mode 100644 index dc5286174d8..00000000000 --- a/app/assets/javascripts/work_items/graphql/change_work_item_parent_link.mutation.graphql +++ /dev/null @@ -1,13 +0,0 @@ -mutation changeWorkItemParentLink($id: WorkItemID!, $parentId: WorkItemID) { - workItemUpdate(input: { id: $id, hierarchyWidget: { parentId: $parentId } }) { - workItem { - id - workItemType { - id - } - title - state - } - errors - } -} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 00ae509ad15..c33882a9ca4 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -478,6 +478,11 @@ span.idiff { background-color: transparent; border: transparent; } + + .gl-dark & { + background: transparent; + filter: invert(1) hue-rotate(180deg); + } } .code-navigation-line:hover { diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 14cd53d17cd..2258491d2e0 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -390,7 +390,7 @@ module Ci end def self.latest_status(ref = nil) - newest_first(ref: ref).pluck(:status).first + newest_first(ref: ref).pick(:status) end def self.latest_successful_for_ref(ref) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index a4d9bed70df..32536b38895 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -430,8 +430,7 @@ class MergeRequest < ApplicationRecord def self.total_time_to_merge join_metrics .merge(MergeRequest::Metrics.with_valid_time_to_merge) - .pluck(MergeRequest::Metrics.time_to_merge_expression) - .first + .pick(MergeRequest::Metrics.time_to_merge_expression) end after_save :keep_around_commit, unless: :importing? diff --git a/app/models/merge_request/approval_removal_settings.rb b/app/models/merge_request/approval_removal_settings.rb new file mode 100644 index 00000000000..b07242e2578 --- /dev/null +++ b/app/models/merge_request/approval_removal_settings.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +class MergeRequest::ApprovalRemovalSettings # rubocop:disable Style/ClassAndModuleChildren + include ActiveModel::Validations + + attr_accessor :project + + validate :mutually_exclusive_settings + + def initialize(project, reset_approvals_on_push, selective_code_owner_removals) + @project = project + @reset_approvals_on_push = reset_approvals_on_push + @selective_code_owner_removals = selective_code_owner_removals + end + + private + + def selective_code_owner_removals + if @selective_code_owner_removals.nil? + project.project_setting.selective_code_owner_removals + else + @selective_code_owner_removals + end + end + + def reset_approvals_on_push + if @reset_approvals_on_push.nil? + project.reset_approvals_on_push + else + @reset_approvals_on_push + end + end + + def mutually_exclusive_settings + return unless selective_code_owner_removals && reset_approvals_on_push + + errors.add(:base, 'selective_code_owner_removals can only be enabled when reset_approvals_on_push is disabled') + end +end diff --git a/app/models/merge_request/metrics.rb b/app/models/merge_request/metrics.rb index b984228eb13..c546a5a0025 100644 --- a/app/models/merge_request/metrics.rb +++ b/app/models/merge_request/metrics.rb @@ -41,8 +41,7 @@ class MergeRequest::Metrics < ApplicationRecord def self.total_time_to_merge with_valid_time_to_merge - .pluck(time_to_merge_expression) - .first + .pick(time_to_merge_expression) end end diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 89c61ea6ebd..9f7e98dc04b 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -706,8 +706,7 @@ class MergeRequestDiff < ApplicationRecord latest_id = MergeRequest .where(id: merge_request_id) .limit(1) - .pluck(:latest_merge_request_diff_id) - .first + .pick(:latest_merge_request_diff_id) latest_id && self.id < latest_id end diff --git a/db/migrate/20220619212618_add_selective_code_owner_removals_to_project_settings.rb b/db/migrate/20220619212618_add_selective_code_owner_removals_to_project_settings.rb new file mode 100644 index 00000000000..435a1d7a40e --- /dev/null +++ b/db/migrate/20220619212618_add_selective_code_owner_removals_to_project_settings.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddSelectiveCodeOwnerRemovalsToProjectSettings < Gitlab::Database::Migration[2.0] + enable_lock_retries! + + def change + add_column :project_settings, :selective_code_owner_removals, :boolean, default: false, null: false + end +end diff --git a/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb b/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb index 7d4d97acf58..0f6cf970778 100644 --- a/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb +++ b/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb @@ -1,23 +1,12 @@ # frozen_string_literal: true class ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl < ActiveRecord::Migration[6.0] - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - MIGRATION = 'UpdateJiraTrackerDataDeploymentTypeBasedOnUrl' - DELAY_INTERVAL = 2.minutes.to_i - BATCH_SIZE = 2_500 - - disable_ddl_transaction! - def up - say "Scheduling #{MIGRATION} jobs" - queue_background_migration_jobs_by_range_at_intervals( - define_batchable_model('jira_tracker_data'), - MIGRATION, - DELAY_INTERVAL, - batch_size: BATCH_SIZE - ) + # no-op (being re-run in 20220324152945_update_jira_tracker_data_deployment_type_based_on_url.rb) + # due to this migration causing this issue: https://gitlab.com/gitlab-org/gitlab/-/issues/336849 + # The migration is rescheduled in + # db/post_migrate/20220725150127_update_jira_tracker_data_deployment_type_based_on_url.rb + # Related discussion: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82103#note_862401816 end def down diff --git a/db/post_migrate/20210825193652_backfill_cadence_id_for_boards_scoped_to_iteration.rb b/db/post_migrate/20210825193652_backfill_cadence_id_for_boards_scoped_to_iteration.rb index f350fbe3d12..1f6650140d4 100644 --- a/db/post_migrate/20210825193652_backfill_cadence_id_for_boards_scoped_to_iteration.rb +++ b/db/post_migrate/20210825193652_backfill_cadence_id_for_boards_scoped_to_iteration.rb @@ -20,7 +20,7 @@ class BackfillCadenceIdForBoardsScopedToIteration < Gitlab::Database::Migration[ def down MigrationBoard.where.not(iteration_cadence_id: nil).each_batch(of: BATCH_SIZE) do |batch, index| - range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + range = batch.pick(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')) delay = index * DELAY migrate_in(delay, MIGRATION, ['none', 'down', *range]) @@ -31,7 +31,7 @@ class BackfillCadenceIdForBoardsScopedToIteration < Gitlab::Database::Migration[ def schedule_backfill_project_boards MigrationBoard.where(iteration_id: -4).where.not(project_id: nil).where(iteration_cadence_id: nil).each_batch(of: BATCH_SIZE) do |batch, index| - range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + range = batch.pick(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')) delay = index * DELAY migrate_in(delay, MIGRATION, ['project', 'up', *range]) @@ -40,7 +40,7 @@ class BackfillCadenceIdForBoardsScopedToIteration < Gitlab::Database::Migration[ def schedule_backfill_group_boards MigrationBoard.where(iteration_id: -4).where.not(group_id: nil).where(iteration_cadence_id: nil).each_batch(of: BATCH_SIZE) do |batch, index| - range = batch.pluck(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')).first + range = batch.pick(Arel.sql('MIN(id)'), Arel.sql('MAX(id)')) delay = index * DELAY migrate_in(delay, MIGRATION, ['group', 'up', *range]) diff --git a/db/post_migrate/20220213103859_remove_integrations_type.rb b/db/post_migrate/20220213103859_remove_integrations_type.rb index c3633d1e7d3..3c420760a2d 100644 --- a/db/post_migrate/20220213103859_remove_integrations_type.rb +++ b/db/post_migrate/20220213103859_remove_integrations_type.rb @@ -73,7 +73,7 @@ class RemoveIntegrationsType < Gitlab::Database::Migration[1.0] add_concurrent_index :integrations, :id, where: 'type_new is null', name: tmp_index_name define_batchable_model(:integrations).where(type_new: nil).each_batch do |batch| - min_id, max_id = batch.pluck(Arel.sql('MIN(id), MAX(id)')).first + min_id, max_id = batch.pick(Arel.sql('MIN(id), MAX(id)')) connection.execute(<<~SQL) WITH mapping(old_type, new_type) AS (VALUES diff --git a/db/post_migrate/20220412143552_consume_remaining_encrypt_integration_property_jobs.rb b/db/post_migrate/20220412143552_consume_remaining_encrypt_integration_property_jobs.rb index 69850b3a32f..2213268fa73 100644 --- a/db/post_migrate/20220412143552_consume_remaining_encrypt_integration_property_jobs.rb +++ b/db/post_migrate/20220412143552_consume_remaining_encrypt_integration_property_jobs.rb @@ -12,7 +12,7 @@ class ConsumeRemainingEncryptIntegrationPropertyJobs < Gitlab::Database::Migrati relation = model.where.not(properties: nil).where(encrypted_properties: nil) relation.each_batch(of: BATCH_SIZE) do |batch| - range = batch.pluck('MIN(id)', 'MAX(id)').first + range = batch.pick('MIN(id)', 'MAX(id)') Gitlab::BackgroundMigration::EncryptIntegrationProperties.new.perform(*range) end diff --git a/db/post_migrate/20220524074947_finalize_backfill_null_note_discussion_ids.rb b/db/post_migrate/20220524074947_finalize_backfill_null_note_discussion_ids.rb index f11846ebe1d..45dbc028b18 100644 --- a/db/post_migrate/20220524074947_finalize_backfill_null_note_discussion_ids.rb +++ b/db/post_migrate/20220524074947_finalize_backfill_null_note_discussion_ids.rb @@ -12,7 +12,7 @@ class FinalizeBackfillNullNoteDiscussionIds < Gitlab::Database::Migration[2.0] Gitlab::BackgroundMigration.steal(MIGRATION) define_batchable_model('notes').where(discussion_id: nil).each_batch(of: BATCH_SIZE) do |batch| - range = batch.pluck('MIN(id)', 'MAX(id)').first + range = batch.pick('MIN(id)', 'MAX(id)') Gitlab::BackgroundMigration::BackfillNoteDiscussionId.new.perform(*range) end diff --git a/db/post_migrate/20220725150127_update_jira_tracker_data_deployment_type_based_on_url.rb b/db/post_migrate/20220725150127_update_jira_tracker_data_deployment_type_based_on_url.rb new file mode 100644 index 00000000000..0deba9b3e81 --- /dev/null +++ b/db/post_migrate/20220725150127_update_jira_tracker_data_deployment_type_based_on_url.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class UpdateJiraTrackerDataDeploymentTypeBasedOnUrl < Gitlab::Database::Migration[2.0] + MIGRATION = 'UpdateJiraTrackerDataDeploymentTypeBasedOnUrl' + DELAY_INTERVAL = 2.minutes.to_i + BATCH_SIZE = 2_500 + SUB_BATCH_SIZE = 2_500 + + disable_ddl_transaction! + restrict_gitlab_migration gitlab_schema: :gitlab_main + + def up + say "Scheduling #{MIGRATION} jobs" + delete_queued_jobs(MIGRATION) + queue_batched_background_migration( + MIGRATION, + :jira_tracker_data, + :id, + job_interval: DELAY_INTERVAL, + batch_size: BATCH_SIZE, + sub_batch_size: SUB_BATCH_SIZE + ) + end + + def down + # no-op + end +end diff --git a/db/schema_migrations/20220619212618 b/db/schema_migrations/20220619212618 new file mode 100644 index 00000000000..1ecfdb6aed4 --- /dev/null +++ b/db/schema_migrations/20220619212618 @@ -0,0 +1 @@ +34a9ec48e8480f3a235089f01944f60e93e4b87909a660f18a42bc47a3a0fe51
\ No newline at end of file diff --git a/db/schema_migrations/20220725150127 b/db/schema_migrations/20220725150127 new file mode 100644 index 00000000000..3cbc80d8883 --- /dev/null +++ b/db/schema_migrations/20220725150127 @@ -0,0 +1 @@ +78563f41df5a49803c59b4e41845c985fd1e5f19b1050998fb78d53a9dfe7a28
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3b36e4546cd..75c00d0c09b 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -19723,6 +19723,7 @@ CREATE TABLE project_settings ( legacy_open_source_license_available boolean DEFAULT true NOT NULL, target_platforms character varying[] DEFAULT '{}'::character varying[] NOT NULL, enforce_auth_checks_on_uploads boolean DEFAULT true NOT NULL, + selective_code_owner_removals boolean DEFAULT false NOT NULL, CONSTRAINT check_3a03e7557a CHECK ((char_length(previous_default_branch) <= 4096)), CONSTRAINT check_b09644994b CHECK ((char_length(squash_commit_template) <= 500)), CONSTRAINT check_bde223416c CHECK ((show_default_award_emojis IS NOT NULL)), diff --git a/doc/user/project/merge_requests/approvals/settings.md b/doc/user/project/merge_requests/approvals/settings.md index c07582c2ae6..f677b47598b 100644 --- a/doc/user/project/merge_requests/approvals/settings.md +++ b/doc/user/project/merge_requests/approvals/settings.md @@ -19,7 +19,9 @@ To view or edit merge request approval settings: 1. Go to your project and select **Settings > General**. 1. Expand **Merge request (MR) approvals**. -In this section of general settings, you can configure the following settings: +### Approval settings + +These settings limit who can approve merge requests. | Setting | Description | | ------ | ------ | @@ -27,7 +29,14 @@ In this section of general settings, you can configure the following settings: | [Prevent approvals by users who add commits](#prevent-approvals-by-users-who-add-commits) | When enabled, users who have committed to a merge request cannot approve it. | | [Prevent editing approval rules in merge requests](#prevent-editing-approval-rules-in-merge-requests) | When enabled, users can't override the project's approval rules on merge requests. | | [Require user password to approve](#require-user-password-to-approve) | Force potential approvers to first authenticate with a password. | -| [Remove all approvals when commits are added to the source branch](#remove-all-approvals-when-commits-are-added-to-the-source-branch) | When enabled, remove all existing approvals on a merge request when more changes are added to it. | + +You can further define what happens to existing approvals when commits are added to the merge request. + +| Setting | Description | +| ------ | ------ | +| Keep approvals | Do not remove approvals. | +| [Remove all approvals](#remove-all-approvals-when-commits-are-added-to-the-source-branch) | Remove all existing approvals. | +| [Remove approvals by Code Owners if their files changed](#remove-approvals-by-code-owners-if-their-files-changed) | If a Code Owner has approved the merge request, and the commit changes files they are the Code Owner for, their approval is removed. | ## Prevent approval by author @@ -119,9 +128,21 @@ when more changes are added to it: 1. Select the **Remove all approvals when commits are added to the source branch** checkbox. 1. Select **Save changes**. -Approvals aren't reset when a merge request is [rebased from the UI](../methods/index.md#rebasing-in-semi-linear-merge-methods). +Approvals aren't removed when a merge request is [rebased from the UI](../methods/index.md#rebasing-in-semi-linear-merge-methods) However, approvals are reset if the target branch is changed. +## Remove approvals by Code Owners if their files changed + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90578) in GitLab 15.3. + +If you only want to remove approvals by Code Owners whose files have been changed: + +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Settings > General**. +1. Expand **Merge request (MR) approvals**. +1. Select **Remove approvals by Code Owners if their files changed**. +1. Select **Save changes**. + ## Code coverage check approvals You can require specific approvals if a merge request would result in a decline in code test diff --git a/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb b/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb index 2247747ba08..88746a62d3a 100644 --- a/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb +++ b/lib/gitlab/background_migration/backfill_ci_namespace_mirrors.rb @@ -21,7 +21,7 @@ module Gitlab def perform(start_id, end_id) batch_query = Namespace.base_query.where(id: start_id..end_id) batch_query.each_batch(of: SUB_BATCH_SIZE) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('MIN(id), MAX(id)')).first + first, last = sub_batch.pick(Arel.sql('MIN(id), MAX(id)')) ranged_query = Namespace.unscoped.base_query.where(id: first..last) update_sql = <<~SQL diff --git a/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb b/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb index ff6ab9928b0..2e4c3482e2f 100644 --- a/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb +++ b/lib/gitlab/background_migration/backfill_ci_project_mirrors.rb @@ -20,7 +20,7 @@ module Gitlab def perform(start_id, end_id) batch_query = Project.base_query.where(id: start_id..end_id) batch_query.each_batch(of: SUB_BATCH_SIZE) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('MIN(id), MAX(id)')).first + first, last = sub_batch.pick(Arel.sql('MIN(id), MAX(id)')) ranged_query = Project.unscoped.base_query.where(id: first..last) update_sql = <<~SQL diff --git a/lib/gitlab/background_migration/backfill_integrations_type_new.rb b/lib/gitlab/background_migration/backfill_integrations_type_new.rb index 6f33472af7d..b07d9371c19 100644 --- a/lib/gitlab/background_migration/backfill_integrations_type_new.rb +++ b/lib/gitlab/background_migration/backfill_integrations_type_new.rb @@ -27,7 +27,7 @@ module Gitlab def process_sub_batch(sub_batch) # Extract the start/stop IDs from the current sub-batch - sub_start_id, sub_stop_id = sub_batch.pluck(Arel.sql('MIN(id), MAX(id)')).first + sub_start_id, sub_stop_id = sub_batch.pick(Arel.sql('MIN(id), MAX(id)')) # This matches the mapping from the INSERT trigger added in # db/migrate/20210721135638_add_triggers_to_integrations_type_new.rb diff --git a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb index 587de1bcb5a..3b8a452b855 100644 --- a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb +++ b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_children.rb @@ -19,7 +19,7 @@ module Gitlab def perform(start_id, end_id, sub_batch_size) batch_query = Namespace.base_query.where(id: start_id..end_id) batch_query.each_batch(of: sub_batch_size) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + first, last = sub_batch.pick(Arel.sql('min(id), max(id)')) ranged_query = Namespace.unscoped.base_query.where(id: first..last) update_sql = <<~SQL diff --git a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb index 1c0a83285a6..c69289fb91f 100644 --- a/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb +++ b/lib/gitlab/background_migration/backfill_namespace_traversal_ids_roots.rb @@ -22,7 +22,7 @@ module Gitlab .where("traversal_ids = '{}'") ranged_query.each_batch(of: sub_batch_size) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + first, last = sub_batch.pick(Arel.sql('min(id), max(id)')) # The query need to be reconstructed because .each_batch modifies the default scope # See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510 diff --git a/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb b/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb index a16efa4222b..32962f2bb89 100644 --- a/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb +++ b/lib/gitlab/background_migration/backfill_work_item_type_id_for_issues.rb @@ -20,7 +20,7 @@ module Gitlab parent_batch_relation = relation_scoped_to_range(batch_table, batch_column, start_id, end_id, base_type) parent_batch_relation.each_batch(column: batch_column, of: sub_batch_size) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + first, last = sub_batch.pick(Arel.sql('min(id), max(id)')) # The query need to be reconstructed because .each_batch modifies the default scope # See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510 diff --git a/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb index 68be42dc0a0..12fd9ae7161 100644 --- a/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb +++ b/lib/gitlab/background_migration/batching_strategies/backfill_project_namespace_per_group_batching_strategy.rb @@ -25,7 +25,7 @@ module Gitlab relation = model_class.where(projects_table[:namespace_id].in(hierarchy_cte_sql)).where("#{quoted_column_name} >= ?", batch_min_value) relation.each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop - next_batch_bounds = batch.pluck(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")).first + next_batch_bounds = batch.pick(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")) break end diff --git a/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy.rb index 5cad9d2e3c4..fc08d2b0ab6 100644 --- a/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy.rb +++ b/lib/gitlab/background_migration/batching_strategies/loose_index_scan_batching_strategy.rb @@ -24,7 +24,7 @@ module Gitlab next_batch_bounds = nil relation.distinct_each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop - next_batch_bounds = batch.pluck(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")).first + next_batch_bounds = batch.pick(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")) break end diff --git a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb index c2f59bf9c76..751e483ad24 100644 --- a/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb +++ b/lib/gitlab/background_migration/batching_strategies/primary_key_batching_strategy.rb @@ -28,7 +28,7 @@ module Gitlab next_batch_bounds = nil relation.each_batch(of: batch_size, column: column_name) do |batch| # rubocop:disable Lint/UnreachableLoop - next_batch_bounds = batch.pluck(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")).first + next_batch_bounds = batch.pick(Arel.sql("MIN(#{quoted_column_name}), MAX(#{quoted_column_name})")) break end diff --git a/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects.rb b/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects.rb index cb9b0e88ef4..4da120769a0 100644 --- a/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects.rb +++ b/lib/gitlab/background_migration/cleanup_orphaned_lfs_objects_projects.rb @@ -62,7 +62,7 @@ module Gitlab batch = LfsObjectsProject.where(id: start_id..end_id) batch.each_batch(of: SUB_BATCH_SIZE) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(lfs_objects_projects.id), max(lfs_objects_projects.id)')).first + first, last = sub_batch.pick(Arel.sql('min(lfs_objects_projects.id), max(lfs_objects_projects.id)')) lfs_objects_without_association = LfsObjectsProject diff --git a/lib/gitlab/background_migration/drop_invalid_security_findings.rb b/lib/gitlab/background_migration/drop_invalid_security_findings.rb index 87551bb1b1e..000628e109c 100644 --- a/lib/gitlab/background_migration/drop_invalid_security_findings.rb +++ b/lib/gitlab/background_migration/drop_invalid_security_findings.rb @@ -19,7 +19,7 @@ module Gitlab .no_uuid ranged_query.each_batch(of: sub_batch_size) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + first, last = sub_batch.pick(Arel.sql('min(id), max(id)')) # The query need to be reconstructed because .each_batch modifies the default scope # See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510 diff --git a/lib/gitlab/background_migration/encrypt_static_object_token.rb b/lib/gitlab/background_migration/encrypt_static_object_token.rb index a087d2529eb..e1805d40bab 100644 --- a/lib/gitlab/background_migration/encrypt_static_object_token.rb +++ b/lib/gitlab/background_migration/encrypt_static_object_token.rb @@ -23,7 +23,7 @@ module Gitlab .without_static_object_token_encrypted ranged_query.each_batch(of: BATCH_SIZE) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + first, last = sub_batch.pick(Arel.sql('min(id), max(id)')) batch_query = User.unscoped .where(id: first..last) diff --git a/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb b/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb index 8f785476aa0..6de2187b8e3 100644 --- a/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb +++ b/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb @@ -29,7 +29,7 @@ module Gitlab def perform(start_id, end_id) scope(start_id, end_id).each_batch(of: SUB_BATCH_SIZE, column: :issue_id) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(issue_id), max(issue_id)')).first + first, last = sub_batch.pick(Arel.sql('min(issue_id), max(issue_id)')) # The query need to be reconstructed because .each_batch modifies the default scope # See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510 diff --git a/lib/gitlab/background_migration/populate_vulnerability_reads.rb b/lib/gitlab/background_migration/populate_vulnerability_reads.rb index 5e6475a3d1a..656c62d9ee5 100644 --- a/lib/gitlab/background_migration/populate_vulnerability_reads.rb +++ b/lib/gitlab/background_migration/populate_vulnerability_reads.rb @@ -10,7 +10,7 @@ module Gitlab def perform(start_id, end_id, sub_batch_size) vulnerability_model.where(id: start_id..end_id).each_batch(of: sub_batch_size) do |sub_batch| - first, last = sub_batch.pluck(Arel.sql('min(id), max(id)')).first + first, last = sub_batch.pick(Arel.sql('min(id), max(id)')) connection.execute(insert_query(first, last)) sleep PAUSE_SECONDS diff --git a/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb b/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb index bba1ca26b35..e9a38916999 100644 --- a/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb +++ b/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb @@ -1,42 +1,74 @@ # frozen_string_literal: true # rubocop: disable Style/Documentation -class Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl - # rubocop: disable Gitlab/NamespacedClass - class JiraTrackerData < ActiveRecord::Base - self.table_name = "jira_tracker_data" - self.inheritance_column = :_type_disabled +module Gitlab + module BackgroundMigration + class UpdateJiraTrackerDataDeploymentTypeBasedOnUrl < Gitlab::BackgroundMigration::BatchedMigrationJob + # rubocop: disable Gitlab/NamespacedClass + class JiraTrackerData < ActiveRecord::Base + self.table_name = "jira_tracker_data" + self.inheritance_column = :_type_disabled - include ::Integrations::BaseDataFields - attr_encrypted :url, encryption_options - attr_encrypted :api_url, encryption_options + include ::Integrations::BaseDataFields + attr_encrypted :url, encryption_options + attr_encrypted :api_url, encryption_options - enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment - end - # rubocop: enable Gitlab/NamespacedClass + enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment + end + # rubocop: enable Gitlab/NamespacedClass - # https://rubular.com/r/uwgK7k9KH23efa - JIRA_CLOUD_REGEX = %r{^https?://[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.atlassian\.net$}ix.freeze + # https://rubular.com/r/uwgK7k9KH23efa + JIRA_CLOUD_REGEX = %r{^https?://[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.atlassian\.net$}ix.freeze - # rubocop: disable CodeReuse/ActiveRecord - def perform(start_id, end_id) - trackers_data = JiraTrackerData - .where(deployment_type: 'unknown') - .where(id: start_id..end_id) + def perform + cloud = [] + server = [] + unknown = [] - cloud, server = trackers_data.partition { |tracker_data| tracker_data.url.match?(JIRA_CLOUD_REGEX) } + trackers_data.each do |tracker_data| + client_url = tracker_data.api_url.presence || tracker_data.url - cloud_mappings = cloud.each_with_object({}) do |tracker_data, hash| - hash[tracker_data] = { deployment_type: 2 } - end + if client_url.blank? + unknown << tracker_data + elsif client_url.match?(JIRA_CLOUD_REGEX) + cloud << tracker_data + else + server << tracker_data + end + end - server_mapppings = server.each_with_object({}) do |tracker_data, hash| - hash[tracker_data] = { deployment_type: 1 } - end + cloud_mappings = cloud.each_with_object({}) do |tracker_data, hash| + hash[tracker_data] = { deployment_type: 2 } + end + + server_mappings = server.each_with_object({}) do |tracker_data, hash| + hash[tracker_data] = { deployment_type: 1 } + end + + unknown_mappings = unknown.each_with_object({}) do |tracker_data, hash| + hash[tracker_data] = { deployment_type: 0 } + end - mappings = cloud_mappings.merge(server_mapppings) + mappings = cloud_mappings.merge(server_mappings, unknown_mappings) - ::Gitlab::Database::BulkUpdate.execute(%i[deployment_type], mappings) + update_records(mappings) + end + + private + + def update_records(mappings) + return if mappings.empty? + + ::Gitlab::Database::BulkUpdate.execute(%i[deployment_type], mappings) + end + + # rubocop: disable CodeReuse/ActiveRecord + def trackers_data + @trackers_data ||= JiraTrackerData + .where(deployment_type: 'unknown') + .where(batch_column => start_id..end_id) + end + # rubocop: enable CodeReuse/ActiveRecord + end end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb b/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb index 38932e52bb0..b61f2ee7f4c 100644 --- a/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb +++ b/lib/gitlab/background_migration/update_timelogs_null_spent_at.rb @@ -12,7 +12,7 @@ module Gitlab define_batchable_model('timelogs', connection: connection) .where(spent_at: nil, id: start_id..stop_id) .each_batch(of: 100) do |subbatch| - batch_start, batch_end = subbatch.pluck('min(id), max(id)').first + batch_start, batch_end = subbatch.pick('min(id), max(id)') update_timelogs(batch_start, batch_end) end diff --git a/lib/gitlab/database/dynamic_model_helpers.rb b/lib/gitlab/database/dynamic_model_helpers.rb index ad7dea8f0d9..2deb89a0b84 100644 --- a/lib/gitlab/database/dynamic_model_helpers.rb +++ b/lib/gitlab/database/dynamic_model_helpers.rb @@ -32,7 +32,7 @@ module Gitlab def each_batch_range(table_name, connection:, scope: ->(table) { table.all }, of: BATCH_SIZE) each_batch(table_name, connection: connection, scope: scope, of: of) do |batch| - yield batch.pluck('MIN(id), MAX(id)').first + yield batch.pick('MIN(id), MAX(id)') end end end diff --git a/lib/gitlab/database/migrations/background_migration_helpers.rb b/lib/gitlab/database/migrations/background_migration_helpers.rb index 9bffed43077..25e75a10bb3 100644 --- a/lib/gitlab/database/migrations/background_migration_helpers.rb +++ b/lib/gitlab/database/migrations/background_migration_helpers.rb @@ -76,7 +76,7 @@ module Gitlab max = relation.arel_table[primary_column_name].maximum min = relation.arel_table[primary_column_name].minimum - start_id, end_id = relation.pluck(min, max).first + start_id, end_id = relation.pick(min, max) # `SingleDatabaseWorker.bulk_perform_in` schedules all jobs for # the same time, which is not helpful in most cases where we wish to diff --git a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb index 9cab2c51b3f..dcf457b9d63 100644 --- a/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb +++ b/lib/gitlab/database/partitioning_migration_helpers/backfill_partitioned_table.rb @@ -25,7 +25,7 @@ module Gitlab parent_batch_relation = relation_scoped_to_range(source_table, source_column, start_id, stop_id) parent_batch_relation.each_batch(of: SUB_BATCH_SIZE) do |sub_batch| - sub_start_id, sub_stop_id = sub_batch.pluck(Arel.sql("MIN(#{source_column}), MAX(#{source_column})")).first + sub_start_id, sub_stop_id = sub_batch.pick(Arel.sql("MIN(#{source_column}), MAX(#{source_column})")) bulk_copy.copy_between(sub_start_id, sub_stop_id) sleep(PAUSE_SECONDS) diff --git a/lib/gitlab/github_import/user_finder.rb b/lib/gitlab/github_import/user_finder.rb index a288fc1851e..6d6a00d260d 100644 --- a/lib/gitlab/github_import/user_finder.rb +++ b/lib/gitlab/github_import/user_finder.rb @@ -158,7 +158,7 @@ module Gitlab # rubocop: disable CodeReuse/ActiveRecord def query_id_for_github_email(email) - User.by_any_email(email).pluck(:id).first + User.by_any_email(email).pick(:id) end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/gitlab/relative_positioning/item_context.rb b/lib/gitlab/relative_positioning/item_context.rb index ac0598d8d34..c9392636ef1 100644 --- a/lib/gitlab/relative_positioning/item_context.rb +++ b/lib/gitlab/relative_positioning/item_context.rb @@ -91,8 +91,7 @@ module Gitlab relation = yield relation if block_given? relation - .pluck(grouping_column, Arel.sql("#{calculation}(relative_position) AS position")) - .first&.last + .pick(grouping_column, Arel.sql("#{calculation}(relative_position) AS position"))&.last end def grouping_column @@ -164,8 +163,7 @@ module Gitlab .from(items_with_next_pos, :items) .where('next_pos IS NULL OR ABS(pos::bigint - next_pos::bigint) >= ?', MIN_GAP) .limit(1) - .pluck(:pos, :next_pos) - .first + .pick(:pos, :next_pos) return if gap.nil? || gap.first == default_end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a8968115161..31387932cb0 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -4792,6 +4792,9 @@ msgstr "" msgid "ApprovalRule|Try for free" msgstr "" +msgid "ApprovalSettings|Keep approvals" +msgstr "" + msgid "ApprovalSettings|Merge request approval settings have been updated." msgstr "" @@ -4810,7 +4813,10 @@ msgstr "" msgid "ApprovalSettings|Prevent editing approval rules in projects and merge requests." msgstr "" -msgid "ApprovalSettings|Remove all approvals when commits are added to the source branch" +msgid "ApprovalSettings|Remove all approvals" +msgstr "" + +msgid "ApprovalSettings|Remove approvals by Code Owners if their files changed" msgstr "" msgid "ApprovalSettings|Require user password to approve" @@ -4828,6 +4834,9 @@ msgstr "" msgid "ApprovalSettings|This setting is configured in %{groupName} and can only be changed in the group settings by an administrator or group owner." msgstr "" +msgid "ApprovalSettings|When a commit is added:" +msgstr "" + msgid "Approvals are optional." msgstr "" @@ -23022,9 +23031,6 @@ msgstr "" msgid "Last successful update" msgstr "" -msgid "Last time checked" -msgstr "" - msgid "Last time verified" msgstr "" @@ -47116,6 +47122,9 @@ msgstr "" msgid "security Reports|There was an error creating the merge request" msgstr "" +msgid "selective_code_owner_removals can only be enabled when retain_approvals_on_push is enabled" +msgstr "" + msgid "severity|Blocker" msgstr "" diff --git a/spec/features/groups/members/manage_members_spec.rb b/spec/features/groups/members/manage_members_spec.rb index 468001c3be6..5f28afc23f1 100644 --- a/spec/features/groups/members/manage_members_spec.rb +++ b/spec/features/groups/members/manage_members_spec.rb @@ -74,8 +74,8 @@ RSpec.describe 'Groups > Members > Manage members' do invite_member(user1.name, role: 'Reporter', refresh: false) - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_content("not authorized to update member") + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_content("not authorized to update member") page.refresh diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index d55e5fbadad..55f17727df7 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -1213,45 +1213,47 @@ paragraph }; it.each` - mark | markdown | modifiedMarkdown | editAction - ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction} - ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction} - ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction} - ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction} - ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction} - ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction} - ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction} - ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction} - ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction} - ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com/">link modified</a>'} | ${defaultEditAction} - ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction} - ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction} - ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction} - ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction} - ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link [https://www.gitlab.com>](https://www.gitlab.com%3E)'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction} - ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction} - ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction} - ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction} - ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction} - ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction} - ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction} - ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction} - ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction} - ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction} - ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction} - ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction} - ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction} - ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction} - ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction} - ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction} - ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction} + mark | markdown | modifiedMarkdown | editAction + ${'bold'} | ${'**bold**'} | ${'**bold modified**'} | ${defaultEditAction} + ${'bold'} | ${'__bold__'} | ${'__bold modified__'} | ${defaultEditAction} + ${'bold'} | ${'<strong>bold</strong>'} | ${'<strong>bold modified</strong>'} | ${defaultEditAction} + ${'bold'} | ${'<b>bold</b>'} | ${'<b>bold modified</b>'} | ${defaultEditAction} + ${'italic'} | ${'_italic_'} | ${'_italic modified_'} | ${defaultEditAction} + ${'italic'} | ${'*italic*'} | ${'*italic modified*'} | ${defaultEditAction} + ${'italic'} | ${'<em>italic</em>'} | ${'<em>italic modified</em>'} | ${defaultEditAction} + ${'italic'} | ${'<i>italic</i>'} | ${'<i>italic modified</i>'} | ${defaultEditAction} + ${'link'} | ${'[gitlab](https://gitlab.com)'} | ${'[gitlab modified](https://gitlab.com)'} | ${defaultEditAction} + ${'link'} | ${'<a href="https://gitlab.com">link</a>'} | ${'<a href="https://gitlab.com/">link modified</a>'} | ${defaultEditAction} + ${'link'} | ${'link www.gitlab.com'} | ${'modified link www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com'} | ${'modified link https://www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link(https://www.gitlab.com)'} | ${'modified link(https://www.gitlab.com)'} | ${prependContentEditAction} + ${'link'} | ${'link(engineering@gitlab.com)'} | ${'modified link(engineering@gitlab.com)'} | ${prependContentEditAction} + ${'link'} | ${'link <https://www.gitlab.com>'} | ${'modified link <https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link [https://www.gitlab.com>'} | ${'modified link \\[https://www.gitlab.com>'} | ${prependContentEditAction} + ${'link'} | ${'link <https://www.gitlab.com'} | ${'modified link <https://www.gitlab.com'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com>'} | ${'modified link [https://www.gitlab.com>](https://www.gitlab.com%3E)'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/path'} | ${'modified link https://www.gitlab.com/path'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com?query=search'} | ${'modified link https://www.gitlab.com?query=search'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/#fragment'} | ${'modified link https://www.gitlab.com/#fragment'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com/?query=search'} | ${'modified link https://www.gitlab.com/?query=search'} | ${prependContentEditAction} + ${'link'} | ${'link https://www.gitlab.com#fragment'} | ${'modified link https://www.gitlab.com#fragment'} | ${prependContentEditAction} + ${'link'} | ${'link **https://www.gitlab.com]**'} | ${'modified link [**https://www.gitlab.com\\]**](https://www.gitlab.com%5D)'} | ${prependContentEditAction} + ${'code'} | ${'`code`'} | ${'`code modified`'} | ${defaultEditAction} + ${'code'} | ${'<code>code</code>'} | ${'<code>code modified</code>'} | ${defaultEditAction} + ${'strike'} | ${'~~striked~~'} | ${'~~striked modified~~'} | ${defaultEditAction} + ${'strike'} | ${'<del>striked</del>'} | ${'<del>striked modified</del>'} | ${defaultEditAction} + ${'strike'} | ${'<strike>striked</strike>'} | ${'<strike>striked modified</strike>'} | ${defaultEditAction} + ${'strike'} | ${'<s>striked</s>'} | ${'<s>striked modified</s>'} | ${defaultEditAction} + ${'list'} | ${'- list item'} | ${'- list item modified'} | ${defaultEditAction} + ${'list'} | ${'* list item'} | ${'* list item modified'} | ${defaultEditAction} + ${'list'} | ${'+ list item'} | ${'+ list item modified'} | ${defaultEditAction} + ${'list'} | ${'- list item 1\n- list item 2'} | ${'- list item 1\n- list item 2 modified'} | ${defaultEditAction} + ${'list'} | ${'2) list item'} | ${'2) list item modified'} | ${defaultEditAction} + ${'list'} | ${'1. list item'} | ${'1. list item modified'} | ${defaultEditAction} + ${'taskList'} | ${'2) [ ] task list item'} | ${'2) [ ] task list item modified'} | ${defaultEditAction} + ${'taskList'} | ${'2) [x] task list item'} | ${'2) [x] task list item modified'} | ${defaultEditAction} + ${'image'} | ${'![image](image.png)'} | ${'![image](image.png) modified'} | ${defaultEditAction} + ${'footnoteReference'} | ${'[^1] footnote\n\n[^1]: footnote definition'} | ${'modified [^1] footnote\n\n[^1]: footnote definition'} | ${prependContentEditAction} `( 'preserves original $mark syntax when sourceMarkdown is available for $markdown', async ({ markdown, modifiedMarkdown, editAction }) => { diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js index 6375d0f7e2e..0455460918c 100644 --- a/spec/frontend/invite_members/components/members_token_select_spec.js +++ b/spec/frontend/invite_members/components/members_token_select_spec.js @@ -5,6 +5,7 @@ import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import * as UserApi from '~/api/user_api'; import MembersTokenSelect from '~/invite_members/components/members_token_select.vue'; +import { VALID_TOKEN_BACKGROUND, INVALID_TOKEN_BACKGROUND } from '~/invite_members/constants'; const label = 'testgroup'; const placeholder = 'Search for a member'; @@ -49,6 +50,39 @@ describe('MembersTokenSelect', () => { }); }); + describe('when there are invalidMembers', () => { + it('adds in the correct class values for the tokens', async () => { + const badToken = { ...user1, class: INVALID_TOKEN_BACKGROUND }; + const goodToken = { ...user2, class: VALID_TOKEN_BACKGROUND }; + + wrapper = createComponent(); + + findTokenSelector().vm.$emit('input', [user1, user2]); + + await waitForPromises(); + + expect(findTokenSelector().props('selectedTokens')).toEqual([user1, user2]); + + await wrapper.setProps({ invalidMembers: { one_1: 'bad stuff' } }); + + expect(findTokenSelector().props('selectedTokens')).toEqual([badToken, goodToken]); + }); + + it('does not change class when invalid members are cleared', async () => { + // arrange - invalidMembers is non-empty and then tokens are added + wrapper = createComponent(); + await wrapper.setProps({ invalidMembers: { one_1: 'bad stuff' } }); + findTokenSelector().vm.$emit('input', [user1, user2]); + await waitForPromises(); + + // act - invalidMembers clears out + await wrapper.setProps({ invalidMembers: {} }); + + // assert - we didn't try to update the tokens + expect(findTokenSelector().props('selectedTokens')).toEqual([user1, user2]); + }); + }); + describe('users', () => { beforeEach(() => { jest.spyOn(UserApi, 'getUsers').mockResolvedValue({ data: allUsers }); diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js index f8471b7f167..69316b7f0ca 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_menu_spec.js @@ -6,7 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue'; -import changeWorkItemParentMutation from '~/work_items/graphql/change_work_item_parent_link.mutation.graphql'; +import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.mutation.graphql'; import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql'; import { WIDGET_TYPE_HIERARCHY } from '~/work_items/constants'; import { workItemHierarchyResponse, changeWorkItemParentMutationResponse } from '../../mock_data'; @@ -87,8 +87,12 @@ describe('WorkItemLinksMenu', () => { await waitForPromises(); expect(mutationHandler).toHaveBeenCalledWith({ - id: WORK_ITEM_ID, - parentId: null, + input: { + id: WORK_ITEM_ID, + hierarchyWidget: { + parentId: null, + }, + }, }); }); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index 46aaf480564..050d3c56727 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -480,14 +480,19 @@ export const changeWorkItemParentMutationResponse = { data: { workItemUpdate: { workItem: { + description: null, id: 'gid://gitlab/WorkItem/2', - workItemType: { - id: 'gid://gitlab/WorkItems::Type/5', - __typename: 'WorkItemType', - }, - title: 'Foo', state: 'OPEN', - __typename: 'WorkItem', + title: 'Foo', + widgets: [ + { + type: 'HIERARCHY', + parent: null, + children: { + nodes: [], + }, + }, + ], }, errors: [], __typename: 'WorkItemUpdatePayload', diff --git a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb index b5122af5cd4..6f75d3faef3 100644 --- a/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb +++ b/spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillSnippetRepositories, :migrat let(:file_name) { 'file_name.rb' } let(:content) { 'content' } - let(:ids) { snippets.pluck('MIN(id)', 'MAX(id)').first } + let(:ids) { snippets.pick('MIN(id)', 'MAX(id)') } let(:service) { described_class.new } subject { service.perform(*ids) } diff --git a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb index b96d3f7f0b5..564d970280c 100644 --- a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb +++ b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb @@ -2,10 +2,26 @@ require 'spec_helper' -RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl, schema: 20210421163509 do - let(:services_table) { table(:services) } - let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') } - let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') } +RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do + let(:integrations_table) { table(:integrations) } + let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') } + let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') } + let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService') } + + let(:table_name) { :jira_tracker_data } + let(:batch_column) { :id } + let(:sub_batch_size) { 1 } + let(:pause_ms) { 0 } + let(:migration) do + described_class.new(start_id: 1, end_id: 10, + batch_table: table_name, batch_column: batch_column, + sub_batch_size: sub_batch_size, pause_ms: pause_ms, + connection: ApplicationRecord.connection) + end + + subject(:perform_migration) do + migration.perform + end before do jira_tracker_data = Class.new(ApplicationRecord) do @@ -27,18 +43,21 @@ RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeB end stub_const('JiraTrackerData', jira_tracker_data) - end - let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) } - let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) } + stub_const('UNKNOWN', 0) + stub_const('SERVER', 1) + stub_const('CLOUD', 2) + end - subject { described_class.new.perform(tracker_data_cloud.id, tracker_data_server.id) } + let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: UNKNOWN) } + let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: UNKNOWN) } + let!(:tracker_data_unknown) { JiraTrackerData.create!(id: 3, service_id: service_jira_unknown.id, url: "", deployment_type: UNKNOWN) } it "changes unknown deployment_types based on URL" do - expect(JiraTrackerData.pluck(:deployment_type)).to eq([0, 0]) + expect(JiraTrackerData.pluck(:deployment_type)).to match_array([UNKNOWN, UNKNOWN, UNKNOWN]) - subject + perform_migration - expect(JiraTrackerData.pluck(:deployment_type)).to eq([2, 1]) + expect(JiraTrackerData.order(:id).pluck(:deployment_type)).to match_array([CLOUD, SERVER, UNKNOWN]) end end diff --git a/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb index 9a59c739ecd..a471b00592c 100644 --- a/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb +++ b/spec/migrations/20220725150127_update_jira_tracker_data_deployment_type_based_on_url_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' require_migration! -RSpec.describe ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do - let(:services_table) { table(:services) } - let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') } - let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') } +RSpec.describe UpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do + let(:integrations_table) { table(:integrations) } + let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') } + let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') } before do jira_tracker_data = Class.new(ApplicationRecord) do @@ -29,20 +29,30 @@ RSpec.describe ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration stub_const('JiraTrackerData', jira_tracker_data) stub_const("#{described_class}::BATCH_SIZE", 1) + stub_const("#{described_class}::SUB_BATCH_SIZE", 1) end + # rubocop:disable Layout/LineLength + # rubocop:disable RSpec/ScatteredLet let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) } let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) } + # rubocop:enable Layout/LineLength + # rubocop:enable RSpec/ScatteredLet around do |example| freeze_time { Sidekiq::Testing.fake! { example.run } } end + let(:migration) { described_class::MIGRATION } # rubocop:disable RSpec/ScatteredLet + it 'schedules background migration' do migrate! - expect(BackgroundMigrationWorker.jobs.size).to eq(2) - expect(described_class::MIGRATION).to be_scheduled_migration(tracker_data_cloud.id, tracker_data_cloud.id) - expect(described_class::MIGRATION).to be_scheduled_migration(tracker_data_server.id, tracker_data_server.id) + expect(migration).to have_scheduled_batched_migration( + table_name: :jira_tracker_data, + column_name: :id, + interval: described_class::DELAY_INTERVAL, + gitlab_schema: :gitlab_main + ) end end diff --git a/spec/models/merge_request/approval_removal_settings_spec.rb b/spec/models/merge_request/approval_removal_settings_spec.rb new file mode 100644 index 00000000000..e84fd403a55 --- /dev/null +++ b/spec/models/merge_request/approval_removal_settings_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequest::ApprovalRemovalSettings do + describe 'validations' do + let(:reset_approvals_on_push) { } + let(:selective_code_owner_removals) { } + + subject { described_class.new(project, reset_approvals_on_push, selective_code_owner_removals) } + + context 'when enabling selective_code_owner_removals and reset_approvals_on_push is disabled' do + let(:project) { create(:project, reset_approvals_on_push: false) } + let(:selective_code_owner_removals) { true } + + it { is_expected.to be_valid } + end + + context 'when enabling selective_code_owner_removals and reset_approvals_on_push is enabled' do + let(:project) { create(:project) } + let(:selective_code_owner_removals) { true } + + it { is_expected.not_to be_valid } + end + + context 'when enabling reset_approvals_on_push and selective_code_owner_removals is disabled' do + let(:project) { create(:project) } + let(:reset_approvals_on_push) { true } + + it { is_expected.to be_valid } + end + + context 'when enabling reset_approvals_on_push and selective_code_owner_removals is enabled' do + let(:project) { create(:project) } + let(:reset_approvals_on_push) { true } + + before do + project.project_setting.update!(selective_code_owner_removals: true) + end + + it { is_expected.not_to be_valid } + end + + context 'when enabling reset_approvals_on_push and selective_code_owner_removals' do + let(:project) { create(:project) } + let(:reset_approvals_on_push) { true } + let(:selective_code_owner_removals) { true } + + it { is_expected.not_to be_valid } + end + end +end diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml index 8d3622ca17d..af93c1b6de2 100644 --- a/spec/requests/api/project_attributes.yml +++ b/spec/requests/api/project_attributes.yml @@ -157,6 +157,7 @@ project_setting: - cve_id_request_enabled - mr_default_target_self - target_platforms + - selective_code_owner_removals build_service_desk_setting: # service_desk_setting unexposed_attributes: diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb index b092df4a116..65540f86d34 100644 --- a/spec/requests/projects/cycle_analytics_events_spec.rb +++ b/spec/requests/projects/cycle_analytics_events_spec.rb @@ -10,8 +10,8 @@ RSpec.describe 'value stream analytics events' do let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do - let(:first_issue_iid) { project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s } - let(:first_mr_iid) { project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s } + let(:first_issue_iid) { project.issues.sort_by_attribute(:created_desc).pick(:iid).to_s } + let(:first_mr_iid) { project.merge_requests.sort_by_attribute(:created_desc).pick(:iid).to_s } before do project.add_developer(user) diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index b56ac5b32c6..42b5c44a1c6 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -11,7 +11,7 @@ module Spec page.within invite_modal_selector do select_members(names) choose_options(role, expires_at) - click_button 'Invite' + submit_invites end page.refresh if refresh @@ -42,11 +42,15 @@ module Spec click_button name choose_options(role, expires_at) - click_button 'Invite' + submit_invites page.refresh end + def submit_invites + click_button 'Invite' + end + def choose_options(role, expires_at) unless role == 'Guest' click_button 'Guest' @@ -92,6 +96,29 @@ module Spec end end + def expect_to_have_successful_invite_indicator(page, user) + expect(page).to have_selector("#{member_token_selector(user.id)} .gl-bg-green-100") + expect(page).not_to have_text("#{user.name}: ") + end + + def expect_to_have_invalid_invite_indicator(page, user) + expect(page).to have_selector("#{member_token_selector(user.id)} .gl-bg-red-100") + expect(page).to have_selector(member_token_error_selector(user.id)) + expect(page).to have_text("#{user.name}: Access level should be greater than or equal to") + end + + def expect_to_have_normal_invite_indicator(page, user) + expect(page).to have_selector(member_token_selector(user.id)) + expect(page).not_to have_selector("#{member_token_selector(user.id)} .gl-bg-red-100") + expect(page).not_to have_selector("#{member_token_selector(user.id)} .gl-bg-green-100") + expect(page).not_to have_text("#{user.name}: ") + end + + def expect_to_have_invite_removed(page, user) + expect(page).not_to have_selector(member_token_selector(user.id)) + expect(page).not_to have_text("#{user.name}: Access level should be greater than or equal to") + end + def expect_to_have_group(group) expect(page).to have_selector("[entity-id='#{group.id}']") end diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb index bca0e02fcdd..9d22b78c71d 100644 --- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb +++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb @@ -147,9 +147,9 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| invite_member(user2.name, role: role, refresh: false) - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_content "#{user2.name}: Access level should be greater than or equal to Developer " \ - "inherited membership from group #{group.name}" + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_content "#{user2.name}: Access level should be greater than or equal to " \ + "Developer inherited membership from group #{group.name}" page.refresh @@ -166,31 +166,44 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| group.add_maintainer(user3) end - it 'shows the user errors and then removes them from the form', :js do + it 'shows the partial user error and success and then removes them from the form', :js do + user4 = create(:user) + user5 = create(:user) + visit subentity_members_page_path - invite_member([user2.name, user3.name], role: role, refresh: false) + invite_member([user2.name, user3.name, user4.name], role: role, refresh: false) + + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_text("The following 2 members couldn't be invited") + expect_to_have_invalid_invite_indicator(invite_modal, user2) + expect_to_have_invalid_invite_indicator(invite_modal, user3) + expect_to_have_successful_invite_indicator(invite_modal, user4) - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_selector(member_token_error_selector(user2.id)) - expect(page).to have_selector(member_token_error_selector(user3.id)) - expect(page).to have_text("The following 2 members couldn't be invited") - expect(page).to have_text("#{user2.name}: Access level should be greater than or equal to") - expect(page).to have_text("#{user3.name}: Access level should be greater than or equal to") + # adds new token, but doesn't submit + select_members(user5.name) + + expect_to_have_normal_invite_indicator(invite_modal, user5) remove_token(user2.id) - expect(page).not_to have_selector(member_token_error_selector(user2.id)) - expect(page).to have_selector(member_token_error_selector(user3.id)) - expect(page).to have_text("The following member couldn't be invited") - expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to") + expect(invite_modal).to have_text("The following member couldn't be invited") + expect_to_have_invite_removed(invite_modal, user2) + expect_to_have_invalid_invite_indicator(invite_modal, user3) + expect_to_have_successful_invite_indicator(invite_modal, user4) + expect_to_have_normal_invite_indicator(invite_modal, user5) remove_token(user3.id) - expect(page).not_to have_selector(member_token_error_selector(user3.id)) - expect(page).not_to have_text("The following member couldn't be invited") - expect(page).not_to have_text("Review the invite errors and try again") - expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to") + expect(invite_modal).not_to have_text("The following member couldn't be invited") + expect(invite_modal).not_to have_text("Review the invite errors and try again") + expect_to_have_invite_removed(invite_modal, user3) + expect_to_have_successful_invite_indicator(invite_modal, user4) + expect_to_have_normal_invite_indicator(invite_modal, user5) + + submit_invites + + expect(page).not_to have_selector(invite_modal_selector) page.refresh @@ -203,6 +216,10 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| expect(page).to have_content('Maintainer') expect(page).not_to have_button('Maintainer') end + + page.within find_invited_member_row(user4.name) do + expect(page).to have_button(role) + end end it 'only shows the error for an invalid formatted email and does not display other member errors', :js do @@ -210,12 +227,12 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| invite_member([user2.name, user3.name, 'bad@email'], role: role, refresh: false) - expect(page).to have_selector(invite_modal_selector) - expect(page).to have_text('email contains an invalid email address') - expect(page).not_to have_text("The following 2 members couldn't be invited") - expect(page).not_to have_text("Review the invite errors and try again") - expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to") - expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to") + invite_modal = page.find(invite_modal_selector) + expect(invite_modal).to have_text('email contains an invalid email address') + expect(invite_modal).not_to have_text("The following 2 members couldn't be invited") + expect(invite_modal).not_to have_text("Review the invite errors and try again") + expect(invite_modal).not_to have_text("#{user2.name}: Access level should be greater than or equal to") + expect(invite_modal).not_to have_text("#{user3.name}: Access level should be greater than or equal to") end end end |