Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
85e3ee161d
commit
2470ea59e4
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
###################
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
6
Gemfile
6
Gemfile
|
|
@ -386,9 +386,9 @@ gem 'gitlab-license', '~> 2.5', feature_category: :shared
|
|||
gem 'rack-attack', '~> 6.7.0' # rubocop:todo Gemfile/MissingFeatureCategory
|
||||
|
||||
# Sentry integration
|
||||
gem 'sentry-ruby', '~> 5.17.3', feature_category: :error_tracking
|
||||
gem 'sentry-rails', '~> 5.17.3', feature_category: :error_tracking
|
||||
gem 'sentry-sidekiq', '~> 5.17.3', feature_category: :error_tracking
|
||||
gem 'sentry-ruby', '~> 5.18.0', feature_category: :error_tracking
|
||||
gem 'sentry-rails', '~> 5.18.0', feature_category: :error_tracking
|
||||
gem 'sentry-sidekiq', '~> 5.18.0', feature_category: :error_tracking
|
||||
|
||||
# PostgreSQL query parsing
|
||||
#
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
16
Gemfile.lock
16
Gemfile.lock
|
|
@ -1701,14 +1701,14 @@ GEM
|
|||
pastel (~> 0.8.0)
|
||||
thor (~> 1.3)
|
||||
tty-command (~> 0.10.1)
|
||||
sentry-rails (5.17.3)
|
||||
sentry-rails (5.18.2)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-ruby (5.17.3)
|
||||
sentry-ruby (~> 5.18.2)
|
||||
sentry-ruby (5.18.2)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
sentry-sidekiq (5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-sidekiq (5.18.2)
|
||||
sentry-ruby (~> 5.18.2)
|
||||
sidekiq (>= 3.0)
|
||||
sexp_processor (4.17.1)
|
||||
shellany (0.0.1)
|
||||
|
|
@ -2265,9 +2265,9 @@ DEPENDENCIES
|
|||
seed-fu (~> 2.3.7)
|
||||
selenium-webdriver (~> 4.21, >= 4.21.1)
|
||||
semver_dialects (~> 3.0)
|
||||
sentry-rails (~> 5.17.3)
|
||||
sentry-ruby (~> 5.17.3)
|
||||
sentry-sidekiq (~> 5.17.3)
|
||||
sentry-rails (~> 5.18.0)
|
||||
sentry-ruby (~> 5.18.0)
|
||||
sentry-sidekiq (~> 5.18.0)
|
||||
shoulda-matchers (~> 5.1.0)
|
||||
sidekiq!
|
||||
sidekiq-cron (~> 1.12.0)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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|
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) | | ✓ | ✓ | ✓ | ✓ | |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -32,6 +32,7 @@ FactoryBot.define do
|
|||
end
|
||||
|
||||
trait :completed do
|
||||
with_reassign_to_user
|
||||
status { 5 }
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
])
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue