Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-08-09 12:07:21 +00:00
parent 85e3ee161d
commit 2470ea59e4
51 changed files with 558 additions and 225 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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]
###################

View File

@ -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'

View File

@ -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'

View File

@ -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
#

View File

@ -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"},

View File

@ -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)

View File

@ -1,6 +1,6 @@
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
import { localeDateFormat, parsePikadayDate, timeFor } from '~/lib/utils/datetime_utility';
import { timeFor, parsePikadayDate, dateInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@ -41,10 +41,10 @@ export default {
},
milestoneDatesAbsolute() {
if (this.milestoneDue) {
return `(${localeDateFormat.asDate.format(this.milestoneDue)})`;
return `(${dateInWords(this.milestoneDue)})`;
}
if (this.milestoneStart) {
return `(${localeDateFormat.asDate.format(this.milestoneStart)})`;
return `(${dateInWords(this.milestoneStart)})`;
}
return '';
},
@ -72,22 +72,23 @@ export default {
};
</script>
<template>
<div ref="milestoneDetails">
<gl-icon :size="16" class="gl-shrink-0 gl-mr-2" name="milestone" />
<span class="milestone-title gl-text-truncate">{{ milestone.title }}</span>
<div ref="milestoneDetails" class="issue-milestone-details">
<gl-icon :size="16" class="gl-mr-2 flex-shrink-0" name="milestone" />
<span class="milestone-title gl-display-inline-block gl-text-truncate">{{
milestone.title
}}</span>
<gl-tooltip :target="() => $refs.milestoneDetails" placement="bottom" class="js-item-milestone">
<div class="gl-font-bold">{{ __('Milestone') }}</div>
<div>{{ milestone.title }}</div>
<div
<span class="gl-font-bold">{{ __('Milestone') }}</span> <br />
<span>{{ milestone.title }}</span> <br />
<span
v-if="milestoneStart || milestoneDue"
:class="{
'gl-text-red-300': isMilestonePastDue,
'gl-text-tertiary': !isMilestonePastDue,
}"
>
<div>{{ milestoneDatesHuman }}</div>
<div>{{ milestoneDatesAbsolute }}</div>
</div>
><span>{{ milestoneDatesHuman }}</span
><br /><span>{{ milestoneDatesAbsolute }}</span>
</span>
</gl-tooltip>
</div>
</template>

View File

@ -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;
},

View File

@ -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

View File

@ -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|

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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'

View File

@ -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

View File

@ -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.

View File

@ -33954,6 +33954,17 @@ Represents total number of work items for the represented states.
| <a id="workitemtypename"></a>`name` | [`String!`](#string) | Name of the work item type. |
| <a id="workitemtypewidgetdefinitions"></a>`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 |
| ---- | ---- | ----------- |
| <a id="workitemtypecountsbystatecountsbystate"></a>`countsByState` | [`WorkItemStateCountsType!`](#workitemstatecountstype) | Total number of work items for the represented states. |
| <a id="workitemtypecountsbystateworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Work item type. |
### `WorkItemWidgetAssignees`
Represents an assignees widget.
@ -34177,6 +34188,7 @@ Represents a hierarchy widget.
| <a id="workitemwidgethierarchyhaschildren"></a>`hasChildren` | [`Boolean!`](#boolean) | Indicates if the work item has children. |
| <a id="workitemwidgethierarchyhasparent"></a>`hasParent` | [`Boolean!`](#boolean) | Indicates if the work item has a parent. |
| <a id="workitemwidgethierarchyparent"></a>`parent` | [`WorkItem`](#workitem) | Parent work item. |
| <a id="workitemwidgethierarchyrolledupcountsbytype"></a>`rolledUpCountsByType` **{warning-solid}** | [`[WorkItemTypeCountsByState!]!`](#workitemtypecountsbystate) | **Introduced** in GitLab 17.3. **Status**: Experiment. Counts of descendant work items by work item type and state. |
| <a id="workitemwidgethierarchytype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
### `WorkItemWidgetIteration`
@ -34400,7 +34412,8 @@ Represents a weight widget.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemwidgetweightrolledupweight"></a>`rolledUpWeight` | [`Int`](#int) | Rolled up weight of the work item. |
| <a id="workitemwidgetweightrolledupcompletedweight"></a>`rolledUpCompletedWeight` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.3. **Status**: Experiment. Rolled up weight from closed descendant work items. |
| <a id="workitemwidgetweightrolledupweight"></a>`rolledUpWeight` **{warning-solid}** | [`Int`](#int) | **Introduced** in GitLab 17.2. **Status**: Experiment. Rolled up weight from descendant work items. |
| <a id="workitemwidgetweighttype"></a>`type` | [`WorkItemWidgetType`](#workitemwidgettype) | Widget type. |
| <a id="workitemwidgetweightweight"></a>`weight` | [`Int`](#int) | Weight of the work item. |
| <a id="workitemwidgetweightwidgetdefinition"></a>`widgetDefinition` | [`WorkItemWidgetDefinitionWeight`](#workitemwidgetdefinitionweight) | Weight widget definition. |

View File

@ -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:

View File

@ -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

View File

@ -62,6 +62,7 @@ The following table lists project permissions available for each role:
|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----:|:--------:|:---------:|:----------:|:-----:|-------|
| [Analytics](analytics/index.md):<br>View [issue analytics](group/issues_analytics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | |
| [Analytics](analytics/index.md):<br>View [value stream analytics](group/value_stream_analytics/index.md) | ✓ | ✓ | ✓ | ✓ | ✓ | |
| [Analytics](analytics/index.md):<br>View [Value Streams Dashboard & AI Impact analytics](analytics/value_streams_dashboard.md) | | ✓ | ✓ | ✓ | ✓ | |
| [Analytics](analytics/index.md):<br>View [DORA metrics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ | |
| [Analytics](analytics/index.md):<br>View [CI/CD analytics](analytics/ci_cd_analytics.md) | | ✓ | ✓ | ✓ | ✓ | |
| [Analytics](analytics/index.md):<br>View [code review analytics](analytics/code_review_analytics.md) | | ✓ | ✓ | ✓ | ✓ | |

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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,

View File

@ -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

View File

@ -32,6 +32,7 @@ FactoryBot.define do
end
trait :completed do
with_reassign_to_user
status { 5 }
end

View File

@ -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

View File

@ -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)');
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
}
)
])
)
)
)

View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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