diff --git a/.gitlab/ci/release-environments/main.gitlab-ci.yml b/.gitlab/ci/release-environments/main.gitlab-ci.yml index c911da4618d..cccf0840d28 100644 --- a/.gitlab/ci/release-environments/main.gitlab-ci.yml +++ b/.gitlab/ci/release-environments/main.gitlab-ci.yml @@ -4,7 +4,7 @@ include: inputs: cng_path: 'build/CNG-mirror' - project: 'gitlab-org/quality/pipeline-common' - ref: '8.18.5' + ref: '8.19.0' file: ci/base.gitlab-ci.yml stages: diff --git a/.gitlab/ci/release-environments/security.gitlab-ci.yml b/.gitlab/ci/release-environments/security.gitlab-ci.yml index 11034a250ac..e072fa80ba6 100644 --- a/.gitlab/ci/release-environments/security.gitlab-ci.yml +++ b/.gitlab/ci/release-environments/security.gitlab-ci.yml @@ -7,7 +7,7 @@ include: inputs: cng_path: 'charts/components/images' - project: 'gitlab-org/quality/pipeline-common' - ref: '8.18.5' + ref: '8.19.0' file: ci/base.gitlab-ci.yml stages: diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 882c7736756..a23f44fd581 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -195,12 +195,6 @@ .if-dot-com-gitlab-org-and-subgroups-merge-train: &if-dot-com-gitlab-org-and-subgroups-merge-train if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train" && $CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org/' -.if-dot-com-gitlab-org-ee-tag: &if-dot-com-gitlab-org-ee-tag - if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_TAG =~ /^v?[\d]+\.[\d]+\.[\d]+[\d\w-]*-ee$/' - -.if-dot-com-gitlab-org-security-ee-tag: &if-dot-com-gitlab-org-security-ee-tag - if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/security/gitlab" && $CI_COMMIT_TAG =~ /^v?[\d]+\.[\d]+\.[\d]+[\d\w-]*-ee$/' - .if-ruby-branch: &if-ruby-branch if: '$CI_COMMIT_BRANCH =~ /^ruby\d+(_\d)*$/ || (($CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_EVENT_TYPE != "merge_train") && $CI_MERGE_REQUEST_LABELS =~ /pipeline:run-in-ruby\d+(_\d)*/)' @@ -3371,8 +3365,6 @@ when: never - <<: *if-merge-request-labels-pipeline-expedite when: never - - <<: *if-dot-com-gitlab-org-ee-tag - when: always - !reference [".releases:rules:canonical-dot-com-gitlab-stable-branch-only", rules] .release-environments:rules:start-release-environments-security-pipeline: @@ -3381,8 +3373,6 @@ when: never - <<: *if-merge-request-labels-pipeline-expedite when: never - - <<: *if-dot-com-gitlab-org-security-ee-tag - when: always - !reference [".releases:rules:canonical-dot-com-security-gitlab-stable-branch-only", rules] ################### diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml index eafe366649d..38be52717c5 100644 --- a/.rubocop_todo/rspec/before_all_role_assignment.yml +++ b/.rubocop_todo/rspec/before_all_role_assignment.yml @@ -643,7 +643,6 @@ RSpec/BeforeAllRoleAssignment: - 'ee/spec/services/vulnerability_issue_links/bulk_create_service_spec.rb' - 'ee/spec/services/vulnerability_issue_links/delete_service_spec.rb' - 'ee/spec/services/vulnerability_merge_request_links/create_service_spec.rb' - - 'ee/spec/services/work_items/widgets/weight_service/update_service_spec.rb' - 'ee/spec/support/shared_contexts/gitlab_team_member_shared_context.rb' - 'ee/spec/support/shared_contexts/project_approval_rules_shared_context.rb' - 'ee/spec/support/shared_examples/controllers/projects/license_scanning_report_comparison_shared_examples.rb' diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index a1daf469fcf..4b5c39d26bc 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -1090,7 +1090,6 @@ RSpec/NamedSubject: - 'ee/spec/services/vulnerability_external_issue_links/create_service_spec.rb' - 'ee/spec/services/vulnerability_feedback/create_service_spec.rb' - 'ee/spec/services/work_items/update_service_spec.rb' - - 'ee/spec/services/work_items/widgets/weight_service/update_service_spec.rb' - 'ee/spec/support/shared_contexts/audit_event_not_licensed_shared_context.rb' - 'ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_shared_examples.rb' - 'ee/spec/tasks/gitlab/license_rake_spec.rb' diff --git a/Gemfile b/Gemfile index 56749cc56cc..d8fab280a55 100644 --- a/Gemfile +++ b/Gemfile @@ -386,9 +386,9 @@ gem 'gitlab-license', '~> 2.5', feature_category: :shared gem 'rack-attack', '~> 6.7.0' # rubocop:todo Gemfile/MissingFeatureCategory # Sentry integration -gem 'sentry-ruby', '~> 5.17.3', feature_category: :error_tracking -gem 'sentry-rails', '~> 5.17.3', feature_category: :error_tracking -gem 'sentry-sidekiq', '~> 5.17.3', feature_category: :error_tracking +gem 'sentry-ruby', '~> 5.18.0', feature_category: :error_tracking +gem 'sentry-rails', '~> 5.18.0', feature_category: :error_tracking +gem 'sentry-sidekiq', '~> 5.18.0', feature_category: :error_tracking # PostgreSQL query parsing # diff --git a/Gemfile.checksum b/Gemfile.checksum index 59a808785f8..7b2fe598a83 100644 --- a/Gemfile.checksum +++ b/Gemfile.checksum @@ -652,9 +652,9 @@ {"name":"seed-fu","version":"2.3.7","platform":"ruby","checksum":"f19673443e9af799b730e3d4eca6a89b39e5a36825015dffd00d02ea3365cf74"}, {"name":"selenium-webdriver","version":"4.23.0","platform":"ruby","checksum":"490aeddee879cfea58a4db6628338d60a905bc56cd5e1a60dfbaa9090a19b801"}, {"name":"semver_dialects","version":"3.4.0","platform":"ruby","checksum":"9625fd343cd47335961ccd71249ad905b1a7c03c514a031b35540bc2946eab59"}, -{"name":"sentry-rails","version":"5.17.3","platform":"ruby","checksum":"017771c42d739c0ad2213a581ca9d005cf543227bc13662cd1ca9909f2429459"}, -{"name":"sentry-ruby","version":"5.17.3","platform":"ruby","checksum":"61791a4b0bb0f95cd87aceeaa1efa6d4ab34d64236c9d5df820478adfe2fbbfc"}, -{"name":"sentry-sidekiq","version":"5.17.3","platform":"ruby","checksum":"d0714a218999e41e38127d0c174e0ee62a32b069f92e85b544e0c2125eca2c58"}, +{"name":"sentry-rails","version":"5.18.2","platform":"ruby","checksum":"4dadf6e7567153e868028d6663a4a88f0cc176b8f30f64b564223df9ee800e27"}, +{"name":"sentry-ruby","version":"5.18.2","platform":"ruby","checksum":"df25cb1bd0c6b23f3266e4a77e9b8efd76d41e1680b07b17d257a99262c10d99"}, +{"name":"sentry-sidekiq","version":"5.18.2","platform":"ruby","checksum":"86d6a303c7d32c0ced214279111000bd65e11384ed8bd62dcf6580c6a3f75261"}, {"name":"sexp_processor","version":"4.17.1","platform":"ruby","checksum":"91110946720307f30bf1d549e90d9a529fef40d1fc471c069c8cca7667015da0"}, {"name":"shellany","version":"0.0.1","platform":"ruby","checksum":"0e127a9132698766d7e752e82cdac8250b6adbd09e6c0a7fbbb6f61964fedee7"}, {"name":"shoulda-matchers","version":"5.1.0","platform":"ruby","checksum":"a01d20589989e9653ab4a28c67d9db2b82bcf0a2496cf01d5e1a95a4aaaf5b07"}, diff --git a/Gemfile.lock b/Gemfile.lock index 9ede923784b..c8282759797 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1701,14 +1701,14 @@ GEM pastel (~> 0.8.0) thor (~> 1.3) tty-command (~> 0.10.1) - sentry-rails (5.17.3) + sentry-rails (5.18.2) railties (>= 5.0) - sentry-ruby (~> 5.17.3) - sentry-ruby (5.17.3) + sentry-ruby (~> 5.18.2) + sentry-ruby (5.18.2) bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) - sentry-sidekiq (5.17.3) - sentry-ruby (~> 5.17.3) + sentry-sidekiq (5.18.2) + sentry-ruby (~> 5.18.2) sidekiq (>= 3.0) sexp_processor (4.17.1) shellany (0.0.1) @@ -2265,9 +2265,9 @@ DEPENDENCIES seed-fu (~> 2.3.7) selenium-webdriver (~> 4.21, >= 4.21.1) semver_dialects (~> 3.0) - sentry-rails (~> 5.17.3) - sentry-ruby (~> 5.17.3) - sentry-sidekiq (~> 5.17.3) + sentry-rails (~> 5.18.0) + sentry-ruby (~> 5.18.0) + sentry-sidekiq (~> 5.18.0) shoulda-matchers (~> 5.1.0) sidekiq! sidekiq-cron (~> 1.12.0) diff --git a/app/assets/javascripts/issuable/components/issue_milestone.vue b/app/assets/javascripts/issuable/components/issue_milestone.vue index 90a5b7984d6..43a5d7d27bf 100644 --- a/app/assets/javascripts/issuable/components/issue_milestone.vue +++ b/app/assets/javascripts/issuable/components/issue_milestone.vue @@ -1,6 +1,6 @@ diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue index 4056f3c90d1..8bc6896a87a 100644 --- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue +++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue @@ -2,12 +2,12 @@ import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { STATUS_CLOSED } from '~/issues/constants'; import { + dateInWords, getTimeRemainingInWords, humanTimeframe, isInFuture, isInPast, isToday, - localeDateFormat, newDateAsLocaleTime, } from '~/lib/utils/datetime_utility'; import { __ } from '~/locale'; @@ -35,7 +35,7 @@ export default { milestoneDate() { if (this.milestone.dueDate) { const { dueDate, startDate } = this.milestone; - const date = localeDateFormat.asDate.format(newDateAsLocaleTime(dueDate)); + const date = dateInWords(newDateAsLocaleTime(dueDate), true); const remainingTime = this.milestoneRemainingTime(dueDate, startDate); return `${date} (${remainingTime})`; } @@ -52,7 +52,7 @@ export default { return humanTimeframe(this.startDate, this.dueDate); } if (this.dueDate) { - return localeDateFormat.asDate.format(newDateAsLocaleTime(this.dueDate)); + return dateInWords(newDateAsLocaleTime(this.dueDate), true); } return null; }, diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue index 4a45f3a9d22..32f75bd9dc2 100644 --- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue +++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue @@ -2,7 +2,7 @@ import { GlIcon, GlDatepicker, GlTooltipDirective, GlLink, GlPopover } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { TYPE_ISSUE } from '~/issues/constants'; -import { formatDate, localeDateFormat, parsePikadayDate } from '~/lib/utils/datetime_utility'; +import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility'; import { __, sprintf } from '~/locale'; import { dateFields, dateTypes, Tracking } from '../../constants'; import { dueDateQueries, startDateQueries } from '../../queries/constants'; @@ -166,7 +166,7 @@ export default { return this.$options.i18n.noDate; } - return localeDateFormat.asDate.format(this.dateValue); + return dateInWords(this.parsedDate, true); }, workspacePath() { return this.issuableType === TYPE_ISSUE diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb index 1f4eaff241d..d31debc1a90 100644 --- a/app/graphql/types/work_items/widgets/hierarchy_type.rb +++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb @@ -32,6 +32,10 @@ module Types field :has_parent, GraphQL::Types::Boolean, null: false, method: :has_parent?, description: 'Indicates if the work item has a parent.' + field :rolled_up_counts_by_type, [Types::WorkItems::WorkItemTypeCountsByStateType], + null: false, description: 'Counts of descendant work items by work item type and state.', + alpha: { milestone: '17.3' } + # rubocop: disable CodeReuse/ActiveRecord def has_children? BatchLoader::GraphQL.for(object.work_item.id).batch(default_value: false) do |ids, loader| diff --git a/app/graphql/types/work_items/work_item_type_counts_by_state_type.rb b/app/graphql/types/work_items/work_item_type_counts_by_state_type.rb new file mode 100644 index 00000000000..3f8d4c5fc95 --- /dev/null +++ b/app/graphql/types/work_items/work_item_type_counts_by_state_type.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Types + module WorkItems + # rubocop: disable Graphql/AuthorizeTypes -- Parent node applies authorization + class WorkItemTypeCountsByStateType < BaseObject + graphql_name 'WorkItemTypeCountsByState' + description 'Represents work item counts for the work item type' + + field :work_item_type, Types::WorkItems::TypeType, null: false, + description: 'Work item type.' + + field :counts_by_state, Types::WorkItemStateCountsType, null: false, + description: 'Total number of work items for the represented states.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/app/models/bulk_imports/configuration.rb b/app/models/bulk_imports/configuration.rb index 6d9f598583e..2c4aa6c14b0 100644 --- a/app/models/bulk_imports/configuration.rb +++ b/app/models/bulk_imports/configuration.rb @@ -19,4 +19,8 @@ class BulkImports::Configuration < ApplicationRecord key: Settings.attr_encrypted_db_key_base_32, mode: :per_attribute_iv, algorithm: 'aes-256-gcm' + + def source_hostname + URI.parse(url).host + end end diff --git a/app/models/import/source_user.rb b/app/models/import/source_user.rb index 93ce06528f8..cd89f865638 100644 --- a/app/models/import/source_user.rb +++ b/app/models/import/source_user.rb @@ -20,7 +20,7 @@ module Import validates :namespace_id, :import_type, :source_hostname, :source_user_identifier, :status, presence: true validates :placeholder_user_id, presence: true, unless: :completed? - validates :reassign_to_user_id, presence: true, if: :reassignment_in_progress? + validates :reassign_to_user_id, presence: true, if: -> { reassignment_in_progress? || completed? } scope :for_namespace, ->(namespace_id) { where(namespace_id: namespace_id) } scope :by_statuses, ->(statuses) { where(status: statuses) } @@ -94,8 +94,8 @@ module Import end end - def accepted_reassign_to_user - reassign_to_user if accepted_status? + def mapped_user + accepted_status? ? reassign_to_user : placeholder_user end def accepted_status? diff --git a/app/models/work_items/widgets/hierarchy.rb b/app/models/work_items/widgets/hierarchy.rb index 3964997ca52..d97d4a87078 100644 --- a/app/models/work_items/widgets/hierarchy.rb +++ b/app/models/work_items/widgets/hierarchy.rb @@ -19,6 +19,20 @@ module WorkItems parent.present? end + def rolled_up_counts_by_type + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/474913 + [ + { + work_item_type: WorkItems::Type.default_by_type(:issue), + counts_by_state: { all: 0, opened: 0, closed: 0 } + }, + { + work_item_type: WorkItems::Type.default_by_type(:task), + counts_by_state: { all: 0, opened: 0, closed: 0 } + } + ] + end + def self.quick_action_commands [:set_parent, :add_child, :remove_parent, :remove_child] end diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb index b7b851eb705..c5da7840c71 100644 --- a/app/services/bulk_imports/create_service.rb +++ b/app/services/bulk_imports/create_service.rb @@ -53,6 +53,11 @@ module BulkImports extra: { source_equals_destination: source_equals_destination? } ) + if Feature.enabled?(:importer_user_mapping, current_user) && + Feature.enabled?(:bulk_import_importer_user_mapping, current_user) + ::Import::BulkImports::EphemeralData.new(bulk_import.id).enable_importer_user_mapping + end + BulkImportWorker.perform_async(bulk_import.id) ServiceResponse.success(payload: bulk_import) diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml index 4ddfbecc3d2..4fb2613c55e 100644 --- a/app/views/layouts/_loading_hints.html.haml +++ b/app/views/layouts/_loading_hints.html.haml @@ -1,4 +1,4 @@ -= cache([ActionController::Base.asset_host, user_application_theme, user_color_scheme], expires_in: 1.minute) do += cache([ActionController::Base.asset_host, user_application_color_mode, user_color_scheme, Gitlab::Tracking.enabled?, Gitlab::Tracking.collector_hostname], expires_in: 1.minute) do - css_crossorigin = ActionController::Base.asset_host ? 'anonymous' : nil - if ActionController::Base.asset_host diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml index e5c9dcd604e..d01c438aa3c 100644 --- a/app/views/layouts/devise.html.haml +++ b/app/views/layouts/devise.html.haml @@ -2,7 +2,7 @@ - custom_text = custom_sign_in_description !!! 5 -%html.html-devise-layout{ class: user_application_theme, lang: I18n.locale } +%html.html-devise-layout{ class: user_application_color_mode, lang: I18n.locale } = render "layouts/head" %body.gl-h-full.login-page{ class: "#{system_message_class} #{client_class_list}", data: { page: body_data_page, testid: 'login-page' } } = header_message diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml index ba75ddccf46..93ed36918e0 100644 --- a/app/views/layouts/devise_empty.html.haml +++ b/app/views/layouts/devise_empty.html.haml @@ -1,6 +1,6 @@ - add_page_specific_style 'page_bundles/login' - @with_header = true -- page_classes = [user_application_theme, page_class.flatten.compact] +- page_classes = [user_application_color_mode, page_class.flatten.compact] !!! 5 %html.html-devise-layout{ class: page_classes, lang: I18n.locale } diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index 86bfbc41d18..2271411069f 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ class: [user_application_theme, page_class], lang: I18n.locale } +%html{ class: [user_application_color_mode, page_class], lang: I18n.locale } = render "layouts/head" %body{ class: "#{user_tab_width} #{@body_class} fullscreen-layout", data: body_data } = render 'peek/bar' diff --git a/config/feature_flags/wip/bulk_import_importer_user_mapping.yml b/config/feature_flags/wip/bulk_import_importer_user_mapping.yml new file mode 100644 index 00000000000..dd1a9935a4f --- /dev/null +++ b/config/feature_flags/wip/bulk_import_importer_user_mapping.yml @@ -0,0 +1,9 @@ +--- +name: bulk_import_importer_user_mapping +feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/12378 +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160650 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/478054 +milestone: '17.3' +group: group::import and integrate +type: wip +default_enabled: false diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 08001bd2a2a..db675b7e700 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -1242,6 +1242,7 @@ To allow certain IP ranges (subnets) to bypass all rate limits: - `rate_limit_subnets_allow_list`: Sets the allow list with the IP ranges (subnets) that should bypass all rate limits. For example, `['1.2.3.4/24', '2001:db8::1/32']`. + [Charts example](https://docs.gitlab.com/charts/charts/gitlab/gitlab-pages/index.html#configure-rate-limits-subnets-allow-list) is available. An IPv6 address receives a large prefix in the 128-bit address space. The prefix is typically at least size /64. Because of the large number of possible addresses, if the client's IP address is IPv6, the limit is applied to the IPv6 prefix with a length of 64, rather than the entire IPv6 address. diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d2668d2f805..14720db4300 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -33954,6 +33954,17 @@ Represents total number of work items for the represented states. | `name` | [`String!`](#string) | Name of the work item type. | | `widgetDefinitions` **{warning-solid}** | [`[WorkItemWidgetDefinition!]`](#workitemwidgetdefinition) | **Introduced** in GitLab 16.7. **Status**: Experiment. Available widgets for the work item type. | +### `WorkItemTypeCountsByState` + +Represents work item counts for the work item type. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `countsByState` | [`WorkItemStateCountsType!`](#workitemstatecountstype) | Total number of work items for the represented states. | +| `workItemType` | [`WorkItemType!`](#workitemtype) | Work item type. | + ### `WorkItemWidgetAssignees` Represents an assignees widget. @@ -34177,6 +34188,7 @@ Represents a hierarchy widget. | `hasChildren` | [`Boolean!`](#boolean) | Indicates if the work item has children. | | `hasParent` | [`Boolean!`](#boolean) | Indicates if the work item has a parent. | | `parent` | [`WorkItem`](#workitem) | Parent work item. | +| `rolledUpCountsByType` **{warning-solid}** | [`[WorkItemTypeCountsByState!]!`](#workitemtypecountsbystate) | **Introduced** in GitLab 17.3. **Status**: Experiment. Counts of descendant work items by work item type and state. | | `type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. | ### `WorkItemWidgetIteration` @@ -34400,7 +34412,8 @@ Represents a weight widget. | Name | Type | Description | | ---- | ---- | ----------- | -| `rolledUpWeight` | [`Int`](#int) | Rolled up weight of the work item. | +| `rolledUpCompletedWeight` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Rolled up weight from closed descendant work items. | +| `rolledUpWeight` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.2. **Status**: Experiment. Rolled up weight from descendant work items. | | `type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. | | `weight` | [`Int`](#int) | Weight of the work item. | | `widgetDefinition` | [`WorkItemWidgetDefinitionWeight`](#workitemwidgetdefinitionweight) | Weight widget definition. | diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md index 3cca2a47f9c..bcc255be1b6 100644 --- a/doc/subscriptions/self_managed/index.md +++ b/doc/subscriptions/self_managed/index.md @@ -86,7 +86,7 @@ You can get a list of daily and historical billable users in your GitLab instanc ::HistoricalData.max_historical_user_count(from: 1.year.ago.beginning_of_day, to: Time.current.end_of_day) ``` -#### Update daily and hitorical billable users +#### Update daily and historical billable users Prerequisites: diff --git a/doc/user/gitlab_duo/index.md b/doc/user/gitlab_duo/index.md index 3691e913ebb..9ea9eb87d1e 100644 --- a/doc/user/gitlab_duo/index.md +++ b/doc/user/gitlab_duo/index.md @@ -90,7 +90,7 @@ DETAILS: **Offering:** GitLab.com, Self-managed, GitLab Dedicated - Helps you merge more quickly by generating meaningful commit messages. -- LLM: Vertex AI Codey [`text-bison`](https://console.cloud.google.com/vertex-ai/publishers/google/model-garden/text-bison). +- LLM: Anthropic [Claude 3.5 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet). - [View documentation](../project/merge_requests/duo_in_merge_requests.md#generate-a-merge-commit-message). ### Root cause analysis diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 32854c663cc..5dea059437d 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -62,6 +62,7 @@ The following table lists project permissions available for each role: |----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----:|:--------:|:---------:|:----------:|:-----:|-------| | [Analytics](analytics/index.md):
View [issue analytics](group/issues_analytics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | | [Analytics](analytics/index.md):
View [value stream analytics](group/value_stream_analytics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | | +| [Analytics](analytics/index.md):
View [Value Streams Dashboard & AI Impact analytics](analytics/value_streams_dashboard.md) | | ✓ | ✓ | ✓ | ✓ | | | [Analytics](analytics/index.md):
View [DORA metrics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | | [Analytics](analytics/index.md):
View [CI/CD analytics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | | [Analytics](analytics/index.md):
View [code review analytics](analytics/code_review_analytics.md) | | ✓ | ✓ | ✓ | ✓ | | diff --git a/lib/bulk_imports/groups/graphql/get_projects_query.rb b/lib/bulk_imports/groups/graphql/get_projects_query.rb index 01d2d9776ec..7a7dd21c86e 100644 --- a/lib/bulk_imports/groups/graphql/get_projects_query.rb +++ b/lib/bulk_imports/groups/graphql/get_projects_query.rb @@ -11,10 +11,10 @@ module BulkImports end def to_s - <<-'GRAPHQL' + <<-GRAPHQL query($full_path: ID!, $cursor: String, $per_page: Int) { group(fullPath: $full_path) { - projects(includeSubgroups: false, first: $per_page, after: $cursor) { + projects(includeSubgroups: false, #{not_aimed_for_deletion}first: $per_page, after: $cursor) { page_info: pageInfo { next_page: endCursor has_next_page: hasNextPage @@ -50,6 +50,18 @@ module BulkImports def page_info_path base_path << 'page_info' end + + private + + def source_version + Gitlab::VersionInfo.parse(context.bulk_import.source_version) + end + + def not_aimed_for_deletion + return if source_version < Gitlab::VersionInfo.parse('16.1.0') + + 'notAimedForDeletion: true, ' + end end end end diff --git a/lib/bulk_imports/pipeline/context.rb b/lib/bulk_imports/pipeline/context.rb index fde24cf3646..737f4112002 100644 --- a/lib/bulk_imports/pipeline/context.rb +++ b/lib/bulk_imports/pipeline/context.rb @@ -43,6 +43,18 @@ module BulkImports def configuration @configuration ||= bulk_import.configuration end + + def source_user_mapper + @source_user_mapper ||= Gitlab::Import::SourceUserMapper.new( + namespace: portable.root_ancestor, + import_type: Import::SOURCE_DIRECT_TRANSFER, + source_hostname: configuration.source_hostname + ) + end + + def importer_user_mapping_enabled? + Import::BulkImports::EphemeralData.new(bulk_import_id).importer_user_mapping_enabled? + end end end end diff --git a/lib/gitlab/cache/import/caching.rb b/lib/gitlab/cache/import/caching.rb index beaf9eb666f..7692af6875f 100644 --- a/lib/gitlab/cache/import/caching.rb +++ b/lib/gitlab/cache/import/caching.rb @@ -254,7 +254,7 @@ module Gitlab # Returns the values of the given hash. # - # raw_key - The key of the set to check. + # raw_key - The key of the hash to check. def self.values_from_hash(raw_key) key = cache_key_for(raw_key) @@ -263,6 +263,20 @@ module Gitlab end end + # Returns a single value of the given hash. + # + # raw_key - The key of the hash to check. + # field - The field to get from the hash. + def self.value_from_hash(raw_key, field, timeout: TIMEOUT) + key = cache_key_for(raw_key) + + value = with_redis { |redis| redis.hget(key, field) } + + with_redis { |redis| redis.expire(key, timeout) } if value.present? + + value + end + # Increments value of a field in a hash # # raw_key - The key of the hash to add to. diff --git a/lib/gitlab/import/source_user_mapper.rb b/lib/gitlab/import/source_user_mapper.rb index dc23f890ce8..3a5f9f48cb2 100644 --- a/lib/gitlab/import/source_user_mapper.rb +++ b/lib/gitlab/import/source_user_mapper.rb @@ -11,42 +11,43 @@ module Gitlab @source_hostname = source_hostname end - def find_or_create_internal_user(source_name:, source_username:, source_user_identifier:) - @source_name = source_name - @source_username = source_username - @source_user_identifier = source_user_identifier - - internal_user = find_internal_user - return internal_user if internal_user - - in_lock(lock_key(source_user_identifier), sleep_sec: 2.seconds) do |retried| - if retried - internal_user = find_internal_user - next internal_user if internal_user - end - - create_source_user_mapping - end - end - - private - - attr_reader :namespace, :import_type, :source_hostname, :source_name, :source_username, :source_user_identifier - - def find_internal_user - source_user = ::Import::SourceUser.find_source_user( + def find_source_user(source_user_identifier) + ::Import::SourceUser.find_source_user( source_user_identifier: source_user_identifier, namespace: namespace, source_hostname: source_hostname, import_type: import_type ) - - return unless source_user - - source_user.accepted_reassign_to_user || source_user.placeholder_user end - def create_source_user_mapping + def find_or_create_source_user(source_name:, source_username:, source_user_identifier:) + source_user = find_source_user(source_user_identifier) + + return source_user if source_user + + create_source_user( + source_name: source_name, + source_username: source_username, + source_user_identifier: source_user_identifier + ) + end + + private + + attr_reader :namespace, :import_type, :source_hostname + + def create_source_user(source_name:, source_username:, source_user_identifier:) + in_lock(lock_key(source_user_identifier), sleep_sec: 2.seconds) do |retried| + if retried + source_user = find_source_user(source_user_identifier) + next source_user if source_user + end + + create_source_user_mapping(source_name, source_username, source_user_identifier) + end + end + + def create_source_user_mapping(source_name, source_username, source_user_identifier) ::Import::SourceUser.transaction do import_source_user = ::Import::SourceUser.new( namespace: namespace, @@ -57,15 +58,13 @@ module Gitlab source_hostname: source_hostname ) - internal_user = create_placeholder_user - import_source_user.placeholder_user = internal_user - + import_source_user.placeholder_user = create_placeholder_user(source_name, source_username) import_source_user.save! import_source_user end end - def create_placeholder_user + def create_placeholder_user(source_name, source_username) # If limit is reached, get import user instead, but that's not implemented yet Gitlab::Import::PlaceholderUserCreator.new( import_type: import_type, diff --git a/lib/import/bulk_imports/ephemeral_data.rb b/lib/import/bulk_imports/ephemeral_data.rb new file mode 100644 index 00000000000..68876f298c4 --- /dev/null +++ b/lib/import/bulk_imports/ephemeral_data.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# This class is used to store ephemeral data during a BulkImport. +module Import + module BulkImports + class EphemeralData + def initialize(bulk_import_id) + @bulk_import_id = bulk_import_id + end + + def enable_importer_user_mapping + add('importer_user_mapping', 'enabled') + end + + def importer_user_mapping_enabled? + read('importer_user_mapping') == 'enabled' + end + + private + + attr_reader :bulk_import_id + + def add(field, value) + Gitlab::Cache::Import::Caching.hash_add(cache_key, field, value) + end + + def read(field) + Gitlab::Cache::Import::Caching.value_from_hash(cache_key, field) + end + + def cache_key + "bulk_import_ephemeral_data_#{bulk_import_id}" + end + end + end +end diff --git a/spec/factories/import_source_users.rb b/spec/factories/import_source_users.rb index 9dce9048fd9..079b661cd75 100644 --- a/spec/factories/import_source_users.rb +++ b/spec/factories/import_source_users.rb @@ -32,6 +32,7 @@ FactoryBot.define do end trait :completed do + with_reassign_to_user status { 5 } end diff --git a/spec/features/merge_request/user_accepts_merge_request_spec.rb b/spec/features/merge_request/user_accepts_merge_request_spec.rb index 6e0ad749dbb..733ebe67772 100644 --- a/spec/features/merge_request/user_accepts_merge_request_spec.rb +++ b/spec/features/merge_request/user_accepts_merge_request_spec.rb @@ -88,7 +88,7 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli visit(merge_request_path(merge_request, anchor: 'note_123')) end - it 'accepts a merge request' do + it 'accepts a merge request', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/462685' do check('Delete source branch') click_merge_button diff --git a/spec/frontend/issuable/components/issue_milestone_spec.js b/spec/frontend/issuable/components/issue_milestone_spec.js index 3f2e73fb491..dea98aa32df 100644 --- a/spec/frontend/issuable/components/issue_milestone_spec.js +++ b/spec/frontend/issuable/components/issue_milestone_spec.js @@ -36,25 +36,25 @@ describe('IssueMilestone component', () => { it('renders `Expired` when there is a due date in the past', () => { wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' }); - expect(findTooltip().text()).toContain('Expired 6 months ago (Dec 31, 2019)'); + expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)'); }); it('renders `remaining` when there is a due date in the future', () => { wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' }); - expect(findTooltip().text()).toContain('5 months remaining (Dec 31, 2020)'); + expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)'); }); it('renders `Started` when there is a start date in the past', () => { wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' }); - expect(findTooltip().text()).toContain('Started 6 months ago (Dec 31, 2019)'); + expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)'); }); it('renders `Starts` when there is a start date in the future', () => { wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' }); - expect(findTooltip().text()).toContain('Starts in 5 months (Dec 31, 2020)'); + expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)'); }); }); }); diff --git a/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb b/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb index d79d1b0dbd3..7cc56c8d97f 100644 --- a/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb +++ b/spec/graphql/types/work_items/widgets/hierarchy_type_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Types::WorkItems::Widgets::HierarchyType, feature_category: :team_planning do it 'exposes the expected fields' do - expected_fields = %i[parent children has_children has_parent ancestors type] + expected_fields = %i[parent children has_children has_parent ancestors type rolled_up_counts_by_type] expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/graphql/types/work_items/work_item_type_counts_by_state_type_spec.rb b/spec/graphql/types/work_items/work_item_type_counts_by_state_type_spec.rb new file mode 100644 index 00000000000..64027c4f495 --- /dev/null +++ b/spec/graphql/types/work_items/work_item_type_counts_by_state_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::WorkItems::WorkItemTypeCountsByStateType, feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[work_item_type counts_by_state] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb b/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb index fdbb44c3ec3..52d4cc0bd06 100644 --- a/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb +++ b/spec/lib/bulk_imports/groups/graphql/get_projects_query_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe BulkImports::Groups::Graphql::GetProjectsQuery, feature_category: :importers do - let_it_be(:tracker) { create(:bulk_import_tracker) } + let_it_be(:entity) { create(:bulk_import_entity, :group_entity) } + let_it_be(:tracker) { create(:bulk_import_tracker, entity: entity) } let_it_be(:context) { BulkImports::Pipeline::Context.new(tracker) } subject(:query) { described_class.new(context: context) } @@ -31,4 +32,26 @@ RSpec.describe BulkImports::Groups::Graphql::GetProjectsQuery, feature_category: expect(subject.page_info_path).to eq(expected) end end + + describe '#to_s' do + context 'when the version is >= 16.1' do + before do + entity.bulk_import.update!(source_version: "16.1.0") + end + + it 'includes notAimedForDeletion: true' do + expect(subject.to_s).to include('notAimedForDeletion: true') + end + end + + context 'when the version is < 16.1' do + before do + entity.bulk_import.update!(source_version: "16.0.0") + end + + it 'does not include notAimedForDeletion' do + expect(subject.to_s).not_to include('notAimedForDeletion') + end + end + end end diff --git a/spec/lib/bulk_imports/pipeline/context_spec.rb b/spec/lib/bulk_imports/pipeline/context_spec.rb index 0f1d00172cd..d73a3d4db00 100644 --- a/spec/lib/bulk_imports/pipeline/context_spec.rb +++ b/spec/lib/bulk_imports/pipeline/context_spec.rb @@ -2,10 +2,10 @@ require 'spec_helper' -RSpec.describe BulkImports::Pipeline::Context do +RSpec.describe BulkImports::Pipeline::Context, feature_category: :importers do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group) } - let_it_be(:bulk_import) { create(:bulk_import, user: user) } + let_it_be(:bulk_import) { create(:bulk_import, :with_configuration, user: user) } let_it_be(:project) { create(:project) } let_it_be(:project_entity) { create(:bulk_import_entity, :project_entity, project: project) } let_it_be(:project_tracker) { create(:bulk_import_tracker, entity: project_entity) } @@ -74,4 +74,40 @@ RSpec.describe BulkImports::Pipeline::Context do it { expect(subject.import_export_config).to be_instance_of(BulkImports::FileTransfer::ProjectConfig) } end end + + describe '#source_user_mapper' do + it { expect(subject.source_user_mapper).to be_instance_of(Gitlab::Import::SourceUserMapper) } + + it 'builds with the correct arguments' do + expect(Gitlab::Import::SourceUserMapper).to receive(:new).with( + namespace: group.root_ancestor, + import_type: Import::SOURCE_DIRECT_TRANSFER, + source_hostname: bulk_import.configuration.source_hostname + ) + + subject.source_user_mapper + end + end + + describe '#importer_user_mapping_enabled?' do + subject { described_class.new(tracker, extra: :data).importer_user_mapping_enabled? } + + before do + allow_next_instance_of(Import::BulkImports::EphemeralData, bulk_import.id) do |ephemeral_data| + allow(ephemeral_data).to receive(:importer_user_mapping_enabled?).and_return(status) + end + end + + context 'when importer user mapping is disabled' do + let(:status) { false } + + it { is_expected.to eq(false) } + end + + context 'when importer user mapping is enabled' do + let(:status) { true } + + it { is_expected.to eq(true) } + end + end end diff --git a/spec/lib/gitlab/cache/import/caching_spec.rb b/spec/lib/gitlab/cache/import/caching_spec.rb index 94896180583..c84721eb7cb 100644 --- a/spec/lib/gitlab/cache/import/caching_spec.rb +++ b/spec/lib/gitlab/cache/import/caching_spec.rb @@ -221,13 +221,37 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_shared_state, end end + describe '.value_from_hash' do + it 'returns nil when field was not set' do + expect(described_class.value_from_hash('foo', 'bar')).to eq(nil) + end + + it 'returns the value of the field' do + described_class.hash_add('foo', 'bar', 1) + + expect(described_class.value_from_hash('foo', 'bar')).to eq('1') + end + + it 'refreshes the cache key if a value is present' do + described_class.hash_add('foo', 'bar', 1) + + redis = double(:redis) + + expect(redis).to receive(:hget).with(/foo/, 'bar').and_return('1') + expect(redis).to receive(:expire).with(/foo/, described_class::TIMEOUT) + expect(Gitlab::Redis::SharedState).to receive(:with).twice.and_yield(redis) + + described_class.value_from_hash('foo', 'bar') + end + end + describe '.hash_increment' do it 'increments a value in a hash' do described_class.hash_increment('foo', 'field', 1) described_class.hash_increment('foo', 'field', 5) key = described_class.cache_key_for('foo') - values = Gitlab::Redis::Cache.with { |r| r.hgetall(key) } + values = Gitlab::Redis::SharedState.with { |r| r.hgetall(key) } expect(values).to eq({ 'field' => '6' }) end @@ -237,7 +261,7 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_shared_state, described_class.hash_increment('another-foo', 'another-field', 'not-an-integer') key = described_class.cache_key_for('foo') - values = Gitlab::Redis::Cache.with { |r| r.hgetall(key) } + values = Gitlab::Redis::SharedState.with { |r| r.hgetall(key) } expect(values).to eq({}) end @@ -248,7 +272,7 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_shared_state, described_class.hash_increment('another-foo', 'another-field', -5) key = described_class.cache_key_for('foo') - values = Gitlab::Redis::Cache.with { |r| r.hgetall(key) } + values = Gitlab::Redis::SharedState.with { |r| r.hgetall(key) } expect(values).to eq({}) end @@ -315,7 +339,7 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_shared_state, described_class.list_add('foo', 20) key = described_class.cache_key_for('foo') - values = Gitlab::Redis::Cache.with { |r| r.lrange(key, 0, -1) } + values = Gitlab::Redis::SharedState.with { |r| r.lrange(key, 0, -1) } expect(values).to eq(%w[10 20]) end @@ -328,7 +352,7 @@ RSpec.describe Gitlab::Cache::Import::Caching, :clean_gitlab_redis_shared_state, described_class.list_add('foo', 40, limit: 3) key = described_class.cache_key_for('foo') - values = Gitlab::Redis::Cache.with { |r| r.lrange(key, 0, -1) } + values = Gitlab::Redis::SharedState.with { |r| r.lrange(key, 0, -1) } expect(values).to eq(%w[20 30 40]) end diff --git a/spec/lib/gitlab/import/source_user_mapper_spec.rb b/spec/lib/gitlab/import/source_user_mapper_spec.rb index 001e3a47e0f..881ba0c8aaf 100644 --- a/spec/lib/gitlab/import/source_user_mapper_spec.rb +++ b/spec/lib/gitlab/import/source_user_mapper_spec.rb @@ -3,21 +3,32 @@ require 'spec_helper' RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do - describe '#find_or_create_internal_user' do - let_it_be(:namespace) { create(:namespace) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:import_type) { 'github' } + let_it_be(:source_hostname) { 'github.com' } - let_it_be(:import_type) { 'github' } - let_it_be(:source_hostname) { 'github.com' } - let_it_be(:source_name) { 'Pry Contributor' } - let_it_be(:source_username) { 'a_pry_contributor' } - let_it_be(:source_user_identifier) { '123456' } + let_it_be(:existing_import_source_user) do + create( + :import_source_user, + namespace: namespace, + import_type: import_type, + source_hostname: source_hostname, + source_user_identifier: '101') + end - subject(:find_or_create_internal_user) do + let_it_be(:import_source_user_from_another_import) { create(:import_source_user) } + + describe '#find_or_create_source_user' do + let(:source_name) { 'Pry Contributor' } + let(:source_username) { 'a_pry_contributor' } + let(:source_user_identifier) { '123456' } + + subject(:find_or_create_source_user) do described_class.new( namespace: namespace, import_type: import_type, source_hostname: source_hostname - ).find_or_create_internal_user( + ).find_or_create_source_user( source_name: source_name, source_username: source_username, source_user_identifier: source_user_identifier @@ -25,12 +36,12 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do end shared_examples 'creates an import_source_user and a unique placeholder user' do - it 'creates a import_source_user with an internal placeholder user' do - expect { find_or_create_internal_user }.to change { Import::SourceUser.count }.from(2).to(3) + it 'creates an import_source_user with an internal placeholder user' do + expect { find_or_create_source_user }.to change { Import::SourceUser.count }.by(1) new_import_source_user = Import::SourceUser.last - expect(new_import_source_user.placeholder_user.user_type).to eq('placeholder') + expect(new_import_source_user.placeholder_user).to be_placeholder expect(new_import_source_user.attributes).to include({ 'namespace_id' => namespace.id, 'import_type' => import_type, @@ -42,7 +53,7 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do end it 'creates a new placeholder user with a unique email and username' do - expect { find_or_create_internal_user }.to change { User.where(user_type: :placeholder).count }.by(1) + expect { find_or_create_source_user }.to change { User.where(user_type: :placeholder).count }.by(1) new_placeholder_user = User.where(user_type: :placeholder).last @@ -54,28 +65,18 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do shared_examples 'it does not create an import_source_user or placeholder user' do it 'does not create a import_source_user' do - expect { find_or_create_internal_user }.not_to change { Import::SourceUser.count } + expect { find_or_create_source_user }.not_to change { Import::SourceUser.count } end it 'does not create any internal users' do - expect { find_or_create_internal_user }.not_to change { User.count } + expect { find_or_create_source_user }.not_to change { User.count } end end context 'when the placeholder user limit has not been reached' do - let_it_be(:import_source_user_from_another_import) { create(:import_source_user) } - let_it_be(:different_source_user_from_same_import) do - create(:import_source_user, - namespace_id: namespace.id, - import_type: import_type, - source_hostname: source_hostname, - source_user_identifier: '999999' - ) - end - it_behaves_like 'creates an import_source_user and a unique placeholder user' - context 'when retried and another placeholder user is not created while waiting' do + context 'when retried and another source user is not created while waiting' do before do allow_next_instance_of(described_class) do |source_user_mapper| allow(source_user_mapper).to receive(:in_lock).and_yield(true) @@ -85,16 +86,7 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do it_behaves_like 'creates an import_source_user and a unique placeholder user' end - context 'when retried and another placeholder user was made while waiting' do - let_it_be(:existing_import_source_user) do - create( - :import_source_user, - namespace: namespace, - import_type: import_type, - source_hostname: source_hostname, - source_user_identifier: '123456') - end - + context 'when retried and another source user was made while waiting' do before do allow_next_instance_of(described_class) do |source_user_mapper| allow(source_user_mapper).to receive(:in_lock).and_yield(true) @@ -103,87 +95,44 @@ RSpec.describe Gitlab::Import::SourceUserMapper, feature_category: :importers do allow(Import::SourceUser).to receive(:find_source_user).and_return(nil, existing_import_source_user) end - it 'returns the existing placeholder user' do - expect(find_or_create_internal_user).to eq(existing_import_source_user.placeholder_user) + it 'returns the existing source user' do + expect(find_or_create_source_user).to eq(existing_import_source_user) end it_behaves_like 'it does not create an import_source_user or placeholder user' end context 'and an import source user exists for current import source' do - let_it_be(:existing_import_source_user) do - create( - :import_source_user, - namespace: namespace, - import_type: import_type, - source_hostname: source_hostname, - source_user_identifier: '123456') - end + let(:source_user_identifier) { existing_import_source_user.source_user_identifier } - it 'returns the existing placeholder user' do - expect(find_or_create_internal_user).to eq(existing_import_source_user.placeholder_user) + it 'returns the existing source user' do + expect(find_or_create_source_user).to eq(existing_import_source_user) end it_behaves_like 'it does not create an import_source_user or placeholder user' end - - context 'and the source user does not map to a placeholder user' do - let_it_be(:existing_import_source_user) do - create( - :import_source_user, - :completed, - :with_reassign_to_user, - placeholder_user: nil, - namespace: namespace, - import_type: import_type, - source_hostname: source_hostname, - source_user_identifier: '123456') - end - - it 'returns the existing reassigned user' do - expect(find_or_create_internal_user).to eq(existing_import_source_user.reassign_to_user) - end - - it_behaves_like 'it does not create an import_source_user or placeholder user' - end - - context 'and the source_user maps to a reassigned user' do - let_it_be(:existing_import_source_user) do - create( - :import_source_user, - :with_reassign_to_user, - namespace: namespace, - import_type: import_type, - source_hostname: source_hostname, - source_user_identifier: '123456') - end - - before do - allow_next_found_instance_of(Import::SourceUser) do |source_user| - allow(source_user).to receive(:accepted_status?).and_return(accepted) - end - end - - context 'when reassigned user has accepted the mapping' do - let(:accepted) { true } - - it_behaves_like 'it does not create an import_source_user or placeholder user' - - it 'returns the existing reassign to user' do - expect(find_or_create_internal_user).to eq(existing_import_source_user.reassign_to_user) - end - end - - context 'when reassigned user has not accepted the mapping' do - let(:accepted) { false } - - it_behaves_like 'it does not create an import_source_user or placeholder user' - - it 'returns the existing placeholder user' do - expect(find_or_create_internal_user).to eq(existing_import_source_user.placeholder_user) - end - end - end + end + end + + describe '#find_source_user' do + let(:source_user_identifier) { existing_import_source_user.source_user_identifier } + + subject(:find_source_user) do + described_class.new( + namespace: namespace, + import_type: import_type, + source_hostname: source_hostname + ).find_source_user(source_user_identifier) + end + + it 'returns the existing source user' do + expect(find_source_user).to eq(existing_import_source_user) + end + + context 'when source user does not exist' do + let(:source_user_identifier) { '999999' } + + it { is_expected.to be_nil } end end end diff --git a/spec/lib/import/bulk_imports/ephemeral_data_spec.rb b/spec/lib/import/bulk_imports/ephemeral_data_spec.rb new file mode 100644 index 00000000000..9cccd1df105 --- /dev/null +++ b/spec/lib/import/bulk_imports/ephemeral_data_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::BulkImports::EphemeralData, :clean_gitlab_redis_shared_state, feature_category: :importers do + let(:ephemeral_data) { described_class.new(123) } + + describe '#enable_importer_user_mapping' do + it 'enables importer_user_mapping' do + ephemeral_data.enable_importer_user_mapping + + expect(ephemeral_data.importer_user_mapping_enabled?).to eq(true) + end + end + + describe '#importer_user_mapping_enabled?' do + context 'when importer_user_mapping is enabled' do + before do + ephemeral_data.enable_importer_user_mapping + end + + it 'returns true' do + expect(ephemeral_data.importer_user_mapping_enabled?).to eq(true) + end + end + + context 'when importer_user_mapping is not enabled' do + it 'returns false' do + expect(ephemeral_data.importer_user_mapping_enabled?).to eq(false) + end + end + + context 'when importer_user_mapping is enabled for a different bulk_import_id' do + before do + ephemeral_data.enable_importer_user_mapping + end + + it 'returns false' do + expect(described_class.new(456).importer_user_mapping_enabled?).to eq(false) + end + end + end +end diff --git a/spec/models/bulk_imports/configuration_spec.rb b/spec/models/bulk_imports/configuration_spec.rb index 1cbfef631ac..4518a93fb5c 100644 --- a/spec/models/bulk_imports/configuration_spec.rb +++ b/spec/models/bulk_imports/configuration_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::Configuration, type: :model do +RSpec.describe BulkImports::Configuration, type: :model, feature_category: :importers do describe 'associations' do it { is_expected.to belong_to(:bulk_import).required } end @@ -14,4 +14,12 @@ RSpec.describe BulkImports::Configuration, type: :model do it { is_expected.to validate_presence_of(:url) } it { is_expected.to validate_presence_of(:access_token) } end + + describe '#source_hostname' do + let(:configuration) { described_class.new(url: 'http://example.com/subdir') } + + it 'returns the hostname of the URL' do + expect(configuration.source_hostname).to eq('example.com') + end + end end diff --git a/spec/models/import/source_user_spec.rb b/spec/models/import/source_user_spec.rb index fb97ed1ccb3..4476afb069e 100644 --- a/spec/models/import/source_user_spec.rb +++ b/spec/models/import/source_user_spec.rb @@ -13,18 +13,19 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do it { is_expected.to validate_presence_of(:namespace_id) } it { is_expected.to validate_presence_of(:import_type) } it { is_expected.to validate_presence_of(:placeholder_user_id) } + it { is_expected.not_to validate_presence_of(:reassign_to_user_id) } context 'when completed' do subject { build(:import_source_user, :completed) } it { is_expected.not_to validate_presence_of(:placeholder_user_id) } + it { is_expected.to validate_presence_of(:reassign_to_user_id) } end - it 'validates reassign_to_user_id if status is reassignment_in_progress' do - import_source_user = build(:import_source_user, :reassignment_in_progress, reassign_to_user: nil) + context 'when reassignment_in_progress' do + subject { build(:import_source_user, :reassignment_in_progress) } - expect(import_source_user).to be_invalid - expect(import_source_user.errors[:reassign_to_user_id]).to eq(["can't be blank"]) + it { is_expected.to validate_presence_of(:reassign_to_user_id) } end end @@ -230,10 +231,10 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do end end - describe '#accepted_reassign_to_user' do + describe '#mapped_user' do let_it_be(:source_user) { build(:import_source_user, :with_reassign_to_user) } - subject(:accepted_reassign_to_user) { source_user.accepted_reassign_to_user } + subject(:mapped_user) { source_user.mapped_user } before do allow(source_user).to receive(:accepted_status?).and_return(accepted) @@ -248,7 +249,7 @@ RSpec.describe Import::SourceUser, type: :model, feature_category: :importers do context 'when not accepted' do let(:accepted) { false } - it { is_expected.to be_nil } + it { is_expected.to eq(source_user.placeholder_user) } end end diff --git a/spec/models/work_items/widgets/hierarchy_spec.rb b/spec/models/work_items/widgets/hierarchy_spec.rb index 4d2bcc895c6..00a70b54a30 100644 --- a/spec/models/work_items/widgets/hierarchy_spec.rb +++ b/spec/models/work_items/widgets/hierarchy_spec.rb @@ -88,4 +88,21 @@ RSpec.describe WorkItems::Widgets::Hierarchy, feature_category: :team_planning d end end end + + describe '#rolled_up_counts_by_type' do + subject { described_class.new(work_item_parent).rolled_up_counts_by_type } + + it 'returns placeholder data' do + is_expected.to eq([ + { + work_item_type: WorkItems::Type.default_by_type(:issue), + counts_by_state: { all: 0, opened: 0, closed: 0 } + }, + { + work_item_type: WorkItems::Type.default_by_type(:task), + counts_by_state: { all: 0, opened: 0, closed: 0 } + } + ]) + end + end end diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb index 591d9381dc0..5b762c00563 100644 --- a/spec/requests/api/graphql/work_item_spec.rb +++ b/spec/requests/api/graphql/work_item_spec.rb @@ -182,6 +182,16 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do } hasChildren hasParent + rolledUpCountsByType { + workItemType { + id + } + countsByState { + all + opened + closed + } + } } } GRAPHQL @@ -200,7 +210,25 @@ RSpec.describe 'Query.work_item(id)', feature_category: :team_planning do hash_including('id' => child_link2.work_item.to_gid.to_s) ]) }, 'hasChildren' => true, - 'hasParent' => false + 'hasParent' => false, + 'rolledUpCountsByType' => match_array([ + hash_including( + 'workItemType' => hash_including('id' => anything), + 'countsByState' => { + 'all' => 0, + 'opened' => 0, + 'closed' => 0 + } + ), + hash_including( + 'workItemType' => hash_including('id' => anything), + 'countsByState' => { + 'all' => 0, + 'opened' => 0, + 'closed' => 0 + } + ) + ]) ) ) ) diff --git a/spec/services/bulk_imports/create_service_spec.rb b/spec/services/bulk_imports/create_service_spec.rb index 141f20eb03d..a8fb35243a1 100644 --- a/spec/services/bulk_imports/create_service_spec.rb +++ b/spec/services/bulk_imports/create_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe BulkImports::CreateService, feature_category: :importers do +RSpec.describe BulkImports::CreateService, :clean_gitlab_redis_shared_state, feature_category: :importers do include GraphqlHelpers let(:user) { create(:user) } @@ -369,6 +369,36 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do ) end + it 'enables importer_user_mapping' do + subject.execute + + expect(Import::BulkImports::EphemeralData.new(BulkImport.last.id).importer_user_mapping_enabled?).to eq(true) + end + + context 'when importer_user_mapping feature flag is disable' do + before do + stub_feature_flags(importer_user_mapping: false) + end + + it 'does not enable importer_user_mapping' do + subject.execute + + expect(Import::BulkImports::EphemeralData.new(BulkImport.last.id).importer_user_mapping_enabled?).to eq(false) + end + end + + context 'when bulk_import_importer_user_mapping feature flag is disable' do + before do + stub_feature_flags(bulk_import_importer_user_mapping: false) + end + + it 'does not enable importer_user_mapping' do + subject.execute + + expect(Import::BulkImports::EphemeralData.new(BulkImport.last.id).importer_user_mapping_enabled?).to eq(false) + end + end + context 'on the same instance' do before do allow(Settings.gitlab).to receive(:base_url).and_return('http://gitlab.example') diff --git a/spec/support/shared_examples/views/color_mode_layout_examples.rb b/spec/support/shared_examples/views/color_mode_layout_examples.rb new file mode 100644 index 00000000000..625d07ff116 --- /dev/null +++ b/spec/support/shared_examples/views/color_mode_layout_examples.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +RSpec.shared_examples "a layout which reflects the application color mode setting" do + context 'as a color mode layout' do + let(:default_color_class) { ::Gitlab::ColorModes.default.css_class } + + context 'when no color mode is explicitly selected' do + it 'renders with the default color' do + render + + expect(rendered).to have_selector("html.#{default_color_class}") + end + end + + context 'when user is authenticated & has selected a specific color mode' do + before do + allow(view).to receive(:user_application_color_mode).and_return(chosen_color_mode.css_class) + end + + where(chosen_color_mode: ::Gitlab::ColorModes.available_modes) + + with_them do + it "renders with the #{params[:chosen_color_mode].name} color mode" do + render + + expect(rendered).to have_selector("html.#{chosen_color_mode.css_class}") + end + end + end + end +end diff --git a/spec/views/layouts/devise.html.haml_spec.rb b/spec/views/layouts/devise.html.haml_spec.rb index 102df757072..9b0293fe8d4 100644 --- a/spec/views/layouts/devise.html.haml_spec.rb +++ b/spec/views/layouts/devise.html.haml_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe 'layouts/devise', feature_category: :user_management do - it_behaves_like 'a layout which reflects the application theme setting' + it_behaves_like 'a layout which reflects the application color mode setting' it_behaves_like 'a layout which reflects the preferred language' describe 'logo' do diff --git a/spec/views/layouts/devise_empty.html.haml_spec.rb b/spec/views/layouts/devise_empty.html.haml_spec.rb index 8c4c421922b..973a6efdcac 100644 --- a/spec/views/layouts/devise_empty.html.haml_spec.rb +++ b/spec/views/layouts/devise_empty.html.haml_spec.rb @@ -3,6 +3,6 @@ require 'spec_helper' RSpec.describe 'layouts/devise_empty' do - it_behaves_like 'a layout which reflects the application theme setting' + it_behaves_like 'a layout which reflects the application color mode setting' it_behaves_like 'a layout which reflects the preferred language' end diff --git a/spec/views/layouts/fullscreen.html.haml_spec.rb b/spec/views/layouts/fullscreen.html.haml_spec.rb index c763b53ac6c..626f19c344b 100644 --- a/spec/views/layouts/fullscreen.html.haml_spec.rb +++ b/spec/views/layouts/fullscreen.html.haml_spec.rb @@ -25,6 +25,6 @@ RSpec.describe 'layouts/fullscreen' do expect(rendered).to have_selector(".flash-container.flash-container-no-margin") end - it_behaves_like 'a layout which reflects the application theme setting' + it_behaves_like 'a layout which reflects the application color mode setting' it_behaves_like 'a layout which reflects the preferred language' end