-
-
{{ milestone.title }}
+
+
+
{{
+ milestone.title
+ }}
- {{ __('Milestone') }}
- {{ milestone.title }}
- {{ __('Milestone') }}
+
{{ milestone.title }}
+
- {{ milestoneDatesHuman }}
- {{ milestoneDatesAbsolute }}
-
+ >{{ milestoneDatesHuman }}
{{ milestoneDatesAbsolute }}
+
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