diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index a6b99ebeaeb..a65269de743 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -152,6 +152,12 @@ export default {
isLoading() {
return this.$apollo.queries.boardList.loading;
},
+ totalWeight() {
+ return this.boardList?.totalWeight;
+ },
+ canShowTotalWeight() {
+ return this.weightFeatureAvailable && !this.isLoading;
+ },
},
apollo: {
boardList: {
@@ -356,7 +362,7 @@ export default {
•
- {{ boardList.totalWeight }}
+ {{ totalWeight }}
@@ -381,11 +387,11 @@ export default {
/>
-
+
-
+
- {{ boardList.totalWeight }}
+ {{ totalWeight }}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
index e5f0206bb8b..ec35d8d3aca 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -106,7 +106,7 @@ export default {
@@ -170,7 +170,7 @@ export default {
diff --git a/app/assets/javascripts/pages/projects/shared/save_project_loader.js b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
index aa3589ac88d..7fd9e24549f 100644
--- a/app/assets/javascripts/pages/projects/shared/save_project_loader.js
+++ b/app/assets/javascripts/pages/projects/shared/save_project_loader.js
@@ -1,12 +1,14 @@
-import $ from 'jquery';
-
export default function initProjectLoadingSpinner() {
- const $formContainer = $('.project-edit-container');
- const $loadingSpinner = $('.save-project-loader');
+ const formContainer = document.querySelector('.project-edit-container');
+ if (formContainer == null) {
+ return;
+ }
+
+ const loadingSpinner = document.querySelector('.save-project-loader');
// show loading spinner when saving
- $formContainer.on('ajax:before', () => {
- $formContainer.hide();
- $loadingSpinner.show();
+ formContainer.addEventListener('ajax:before', () => {
+ formContainer.style.display = 'none';
+ loadingSpinner.style.display = 'block';
});
}
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 100ffc0664b..31e80107fd8 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -10,7 +10,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-new-user-signups-cap-reached',
'.js-eoa-bronze-plan-banner',
'.js-security-newsletter-callout',
- '.js-approaching-seats-count-threshold',
+ '.js-approaching-seat-count-threshold',
'.js-storage-enforcement-banner',
'.js-user-over-limit-free-plan-alert',
'.js-minute-limit-banner',
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index ea50099120b..77e734e15fa 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -58,7 +58,16 @@ module Registrations
def path_for_signed_in_user(user)
return users_almost_there_path(email: user.email) if requires_confirmation?(user)
- stored_location_for(user) || members_activity_path(user.members)
+ stored_url = stored_location_for(user)
+ if ::Feature.enabled?(:about_your_company_registration_flow) &&
+ stored_url&.include?(new_users_sign_up_company_path)
+ company_params = update_params.slice(:role, :other_role, :registration_objective)
+ .merge(params.permit(:jobs_to_be_done_other))
+ redirect_uri = Gitlab::Utils.add_url_parameters(stored_url, company_params)
+ store_location_for(:user, redirect_uri)
+ else
+ stored_url || members_activity_path(user.members)
+ end
end
def members_activity_path(members)
diff --git a/app/models/concerns/as_cte.rb b/app/models/concerns/as_cte.rb
new file mode 100644
index 00000000000..aa38ae3a9c1
--- /dev/null
+++ b/app/models/concerns/as_cte.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+# Convert any ActiveRecord::Relation to a Gitlab::SQL::CTE
+module AsCte
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def as_cte(name, **opts)
+ Gitlab::SQL::CTE.new(name, all, **opts)
+ end
+ end
+end
diff --git a/app/models/namespaces/traversal/linear_scopes.rb b/app/models/namespaces/traversal/linear_scopes.rb
index 0321b1f547e..6f404ec12d0 100644
--- a/app/models/namespaces/traversal/linear_scopes.rb
+++ b/app/models/namespaces/traversal/linear_scopes.rb
@@ -5,6 +5,8 @@ module Namespaces
module LinearScopes
extend ActiveSupport::Concern
+ include AsCte
+
class_methods do
# When filtering namespaces by the traversal_ids column to compile a
# list of namespace IDs, it can be faster to reference the ID in
@@ -25,25 +27,15 @@ module Namespaces
def self_and_ancestors(include_self: true, upto: nil, hierarchy_order: nil)
return super unless use_traversal_ids_for_ancestor_scopes?
- ancestors_cte, base_cte = ancestor_ctes
- namespaces = Arel::Table.new(:namespaces)
-
- records = unscoped
- .with(base_cte.to_arel, ancestors_cte.to_arel)
- .distinct
- .from([ancestors_cte.table, namespaces])
- .where(namespaces[:id].eq(ancestors_cte.table[:ancestor_id]))
- .order_by_depth(hierarchy_order)
-
- unless include_self
- records = records.where(ancestors_cte.table[:base_id].not_eq(ancestors_cte.table[:ancestor_id]))
+ if Feature.enabled?(:use_traversal_ids_for_ancestor_scopes_with_inner_join)
+ self_and_ancestors_from_inner_join(include_self: include_self,
+ upto: upto, hierarchy_order:
+ hierarchy_order)
+ else
+ self_and_ancestors_from_ancestors_cte(include_self: include_self,
+ upto: upto,
+ hierarchy_order: hierarchy_order)
end
-
- if upto
- records = records.where.not(id: unscoped.where(id: upto).select('unnest(traversal_ids)'))
- end
-
- records
end
def self_and_ancestor_ids(include_self: true)
@@ -87,7 +79,7 @@ module Namespaces
depth_order = hierarchy_order == :asc ? :desc : :asc
all
- .select(Arel.star, 'array_length(traversal_ids, 1) as depth')
+ .select(Namespace.default_select_columns, 'array_length(traversal_ids, 1) as depth')
.order(depth: depth_order, id: :asc)
end
@@ -125,10 +117,73 @@ module Namespaces
use_traversal_ids?
end
+ def self_and_ancestors_from_ancestors_cte(include_self: true, upto: nil, hierarchy_order: nil)
+ base_cte = all.select('namespaces.id', 'namespaces.traversal_ids').as_cte(:base_ancestors_cte)
+
+ # We have to alias id with 'AS' to avoid ambiguous column references by calling methods.
+ ancestors_cte = unscoped
+ .unscope(where: [:type])
+ .select('id as base_id',
+ "#{unnest_func(base_cte.table['traversal_ids']).to_sql} as ancestor_id")
+ .from(base_cte.table)
+ .as_cte(:ancestors_cte)
+
+ namespaces = Arel::Table.new(:namespaces)
+
+ records = unscoped
+ .with(base_cte.to_arel, ancestors_cte.to_arel)
+ .distinct
+ .from([ancestors_cte.table, namespaces])
+ .where(namespaces[:id].eq(ancestors_cte.table[:ancestor_id]))
+ .order_by_depth(hierarchy_order)
+
+ unless include_self
+ records = records.where(ancestors_cte.table[:base_id].not_eq(ancestors_cte.table[:ancestor_id]))
+ end
+
+ if upto
+ records = records.where.not(id: unscoped.where(id: upto).select('unnest(traversal_ids)'))
+ end
+
+ records
+ end
+
+ def self_and_ancestors_from_inner_join(include_self: true, upto: nil, hierarchy_order: nil)
+ base_cte = all.reselect('namespaces.traversal_ids').as_cte(:base_ancestors_cte)
+
+ unnest = if include_self
+ base_cte.table[:traversal_ids]
+ else
+ base_cte_traversal_ids = 'base_ancestors_cte.traversal_ids'
+ traversal_ids_range = "1:array_length(#{base_cte_traversal_ids},1)-1"
+ Arel.sql("#{base_cte_traversal_ids}[#{traversal_ids_range}]")
+ end
+
+ ancestor_subselect = "SELECT DISTINCT #{unnest_func(unnest).to_sql} FROM base_ancestors_cte"
+ ancestors_join = <<~SQL
+ INNER JOIN (#{ancestor_subselect}) AS ancestors(ancestor_id) ON namespaces.id = ancestors.ancestor_id
+ SQL
+
+ namespaces = Arel::Table.new(:namespaces)
+
+ records = unscoped
+ .with(base_cte.to_arel)
+ .from(namespaces)
+ .joins(ancestors_join)
+ .order_by_depth(hierarchy_order)
+
+ if upto
+ upto_ancestor_ids = unscoped.where(id: upto).select(unnest_func(Arel.sql('traversal_ids')))
+ records = records.where.not(id: upto_ancestor_ids)
+ end
+
+ records
+ end
+
def self_and_descendants_with_comparison_operators(include_self: true)
base = all.select(:traversal_ids)
base = base.select(:id) if Feature.enabled?(:linear_scopes_superset)
- base_cte = Gitlab::SQL::CTE.new(:descendants_base_cte, base)
+ base_cte = base.as_cte(:descendants_base_cte)
namespaces = Arel::Table.new(:namespaces)
@@ -169,6 +224,10 @@ module Namespaces
Arel::Nodes::NamedFunction.new('next_traversal_ids_sibling', args)
end
+ def unnest_func(*args)
+ Arel::Nodes::NamedFunction.new('unnest', args)
+ end
+
def self_and_descendants_with_duplicates_with_array_operator(include_self: true)
base_ids = select(:id)
@@ -197,20 +256,6 @@ module Namespaces
Gitlab::SQL::CTE.new(:superset, superset_sql, materialized: false)
end
-
- def ancestor_ctes
- base_scope = all.select('namespaces.id', 'namespaces.traversal_ids')
- base_cte = Gitlab::SQL::CTE.new(:base_ancestors_cte, base_scope)
-
- # We have to alias id with 'AS' to avoid ambiguous column references by calling methods.
- ancestors_scope = unscoped
- .unscope(where: [:type])
- .select('id as base_id', 'unnest(traversal_ids) as ancestor_id')
- .from(base_cte.table)
- ancestors_cte = Gitlab::SQL::CTE.new(:ancestors_cte, ancestors_scope)
-
- [ancestors_cte, base_cte]
- end
end
end
end
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 11367846b36..3c405dbce3b 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -4,7 +4,50 @@ module WorkItems
class ParentLink < ApplicationRecord
self.table_name = 'work_item_parent_links'
+ MAX_CHILDREN = 100
+
belongs_to :work_item
belongs_to :work_item_parent, class_name: 'WorkItem'
+
+ validates :work_item, :work_item_parent, presence: true
+ validate :validate_child_type
+ validate :validate_parent_type
+ validate :validate_same_project
+ validate :validate_max_children
+
+ private
+
+ def validate_child_type
+ return unless work_item
+
+ unless work_item.task?
+ errors.add :work_item, _('Only Task can be assigned as a child in hierarchy.')
+ end
+ end
+
+ def validate_parent_type
+ return unless work_item_parent
+
+ unless work_item_parent.issue?
+ errors.add :work_item_parent, _('Only Issue can be parent of Task.')
+ end
+ end
+
+ def validate_same_project
+ return if work_item.nil? || work_item_parent.nil?
+
+ if work_item.resource_parent != work_item_parent.resource_parent
+ errors.add :work_item_parent, _('Parent must be in the same project as child.')
+ end
+ end
+
+ def validate_max_children
+ return unless work_item_parent
+
+ max = persisted? ? MAX_CHILDREN : MAX_CHILDREN - 1
+ if work_item_parent.child_links.count > max
+ errors.add :work_item_parent, _('Parent already has maximum number of children.')
+ end
+ end
end
end
diff --git a/app/services/ci/runners/reset_registration_token_service.rb b/app/services/ci/runners/reset_registration_token_service.rb
index 2a3fb08c5e1..81a70a771cf 100644
--- a/app/services/ci/runners/reset_registration_token_service.rb
+++ b/app/services/ci/runners/reset_registration_token_service.rb
@@ -13,11 +13,10 @@ module Ci
def execute
return unless @user.present? && @user.can?(:update_runners_registration_token, scope)
- case scope
- when ::ApplicationSetting
+ if scope.respond_to?(:runners_registration_token)
scope.reset_runners_registration_token!
- ApplicationSetting.current_without_cache.runners_registration_token
- when ::Group, ::Project
+ scope.runners_registration_token
+ else
scope.reset_runners_token!
scope.runners_token
end
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 185ed228d92..b7cf7b7468f 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -16,7 +16,7 @@
= yield :flash_message
= dispensable_render "shared/service_ping_consent"
= dispensable_render_if_exists "layouts/header/ee_subscribable_banner"
- = dispensable_render_if_exists "layouts/header/seats_count_alert"
+ = dispensable_render_if_exists "layouts/header/seat_count_alert"
= dispensable_render_if_exists "shared/namespace_storage_limit_alert"
= dispensable_render_if_exists "shared/namespace_user_cap_reached_alert"
= dispensable_render_if_exists "shared/new_user_signups_cap_reached_alert"
diff --git a/config/feature_flags/development/group_level_protected_environment.yml b/config/feature_flags/development/group_level_protected_environment.yml
new file mode 100644
index 00000000000..f52838f4a46
--- /dev/null
+++ b/config/feature_flags/development/group_level_protected_environment.yml
@@ -0,0 +1,8 @@
+---
+name: group_level_protected_environment
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88506
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/363450
+milestone: '15.1'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/feature_flags/development/seat_count_alerts.yml b/config/feature_flags/development/seat_count_alerts.yml
new file mode 100644
index 00000000000..3b05f391cbc
--- /dev/null
+++ b/config/feature_flags/development/seat_count_alerts.yml
@@ -0,0 +1,8 @@
+---
+name: seat_count_alerts
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79563
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/362041
+milestone: '15.1'
+type: development
+group: group::purchase
+default_enabled: false
diff --git a/config/feature_flags/development/use_traversal_ids_for_ancestor_scopes_with_inner_join.yml b/config/feature_flags/development/use_traversal_ids_for_ancestor_scopes_with_inner_join.yml
new file mode 100644
index 00000000000..a9f7b18acd9
--- /dev/null
+++ b/config/feature_flags/development/use_traversal_ids_for_ancestor_scopes_with_inner_join.yml
@@ -0,0 +1,8 @@
+---
+name: use_traversal_ids_for_ancestor_scopes_with_inner_join
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83371
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/356628
+milestone: '15.1'
+type: development
+group: group::workspace
+default_enabled: false
diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md
index a4ed287cc3b..06eb9ffc84e 100644
--- a/doc/administration/external_pipeline_validation.md
+++ b/doc/administration/external_pipeline_validation.md
@@ -54,7 +54,9 @@ required number of seconds.
"required": [
"id",
"path",
- "created_at"
+ "created_at",
+ "shared_runners_enabled",
+ "group_runners_enabled"
],
"properties": {
"id": { "type": "integer" },
diff --git a/doc/architecture/blueprints/ci_data_decay/index.md b/doc/architecture/blueprints/ci_data_decay/index.md
index cbd9f5dea7a..ed202d00700 100644
--- a/doc/architecture/blueprints/ci_data_decay/index.md
+++ b/doc/architecture/blueprints/ci_data_decay/index.md
@@ -135,6 +135,9 @@ builds archival, to make it consistent and reliable.
Epic: [Partition CI/CD pipelines database tables](https://gitlab.com/groups/gitlab-org/-/epics/5417).
+For more technical details about this topic see
+[pipeline data partitioning design](pipeline_partitioning.md).
+
### Partition CI/CD builds queuing database tables
While working on the [CI/CD Scale](../ci_scale/index.md) blueprint, we have
@@ -159,6 +162,9 @@ business rule is present in the product since the inception of GitLab CI.
Epic: [Partition CI/CD builds queuing database tables](https://gitlab.com/groups/gitlab-org/-/epics/7438).
+For more technical details about this topic see
+[pipeline data partitioning design](pipeline_partitioning.md).
+
## Principles
All the three tracks we will use to implement CI/CD time decay pattern are
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index bb098db7cc1..d592c87d2a8 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -148,12 +148,12 @@ This sync job runs daily around 3AM UTC. If the job fails, it is retried up to 1
The daily job provides **only** the following information to the Customers Portal:
-- Company name
-- Licensee name
-- Licensee email
- Date
- Timestamp
- License key
+ - Company name (encrypted within license key)
+ - Licensee name (encrypted within license key)
+ - Licensee email (encrypted within license key)
- Historical maximum user count
- Billable users count
- GitLab version
diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md
index 6967815bf22..619b697ad12 100644
--- a/doc/user/group/import/index.md
+++ b/doc/user/group/import/index.md
@@ -1,29 +1,28 @@
---
-type: reference, howto
stage: Manage
group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Migrate groups from another instance of GitLab **(FREE)**
+# Migrating groups **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/249160) in GitLab 13.7 [with a flag](../../feature_flags.md) named `bulk_import`. Disabled by default.
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338985) in GitLab 14.3.
FLAG:
-On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `bulk_import`. On GitLab.com, this feature is available.
+On self-managed GitLab, by default this feature is available. To hide the feature, ask an administrator to
+[disable the feature flag](../../../administration/feature_flags.md) named `bulk_import`. On GitLab.com, this feature is
+available.
You can migrate your existing top-level groups to any of the following:
-- Another GitLab instance, including GitLab.com.
- Another top-level group.
- The subgroup of any existing top-level group.
+- Another GitLab instance, including GitLab.com.
-Migrating groups is not the same as [group import/export](../settings/import_export.md).
-
-- Group import/export requires you to export a group to a file and then import that file in
- another GitLab instance.
-- Group migration automates this process.
+Migrating groups using the method documented here is not the same as [migrating groups using file exports](../settings/import_export.md).
+Importing and exporting groups using file exports requires you to export a group to a file and then import that file in
+another GitLab instance. Migrating groups using the method documented here automates this step.
## Import your groups into GitLab
diff --git a/doc/user/group/settings/import_export.md b/doc/user/group/settings/import_export.md
index 23a638fb98c..eb94a181647 100644
--- a/doc/user/group/settings/import_export.md
+++ b/doc/user/group/settings/import_export.md
@@ -1,16 +1,24 @@
---
-type: reference
stage: Manage
group: Import
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# Group import/export **(FREE)**
+# Migrating groups using file exports (deprecated) **(FREE)**
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases.
+> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2888) in GitLab 13.0 as an experimental feature. May change in future releases.
+> - [Deprecated](https://gitlab.com/groups/gitlab-org/-/epics/4619) in GitLab 14.6.
-You can export groups, with all their related data, from one GitLab instance to another.
-You can also [export projects](../../project/settings/import_export.md).
+WARNING:
+This feature was [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/4619) in GitLab 14.6 and replaced by
+[a different migration method](../import/index.md). To follow progress on a solution for
+[offline environments](../../application_security/offline_deployments/index.md), see
+[the relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/363406).
+
+You can export groups, with all their related data, from one GitLab instance to another. You can also:
+
+- [Migrate groups](../import/index.md) using the preferred method.
+- [Migrate projects using file exports](../../project/settings/import_export.md).
## Enable export for a group
@@ -63,10 +71,6 @@ For more details on the specific data persisted in a group export, see the
## Export a group
-WARNING:
-This feature will be [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/4619)
-in GitLab 14.6 and replaced by [GitLab Migration](../import/).
-
Prerequisites:
- You must have the Owner role for the group.
@@ -96,16 +100,11 @@ You can export groups from the [Community Edition to the Enterprise Edition](htt
The Enterprise Edition retains some group data that isn't part of the Community Edition. If you're exporting a group from the Enterprise Edition to the Community Edition, you may lose this data. For more information, see [downgrading from EE to CE](../../../index.md).
-## Importing the group
-
-WARNING:
-This feature will be [deprecated](https://gitlab.com/groups/gitlab-org/-/epics/4619)
-in GitLab 14.8 and replaced by [GitLab Migration](../import/).
+## Import the group
1. Create a new group:
- On the top bar, select **New** (**{plus}**) and then **New group**.
- On an existing group's page, select the **New subgroup** button.
-
1. Select **Import group**.
1. Enter your group name.
1. Accept or modify the associated group URL.
diff --git a/doc/user/group/value_stream_analytics/index.md b/doc/user/group/value_stream_analytics/index.md
index 4be97779603..8908052f4d7 100644
--- a/doc/user/group/value_stream_analytics/index.md
+++ b/doc/user/group/value_stream_analytics/index.md
@@ -366,7 +366,7 @@ The chart shows data for the last 500 workflow items.
- In the **From** field, select a start date.
- In the **To** field, select an end date.
-## Type of work - Tasks by type chart
+## Tasks by type chart
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32421) in GitLab 12.10.
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 97224cae063..4eeb7c5ba83 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -4,10 +4,13 @@ group: Import
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
---
-# Project import/export **(FREE)**
+# Migrating projects using file exports **(FREE)**
Existing projects on any self-managed GitLab instance or GitLab.com can be exported to a file and
-then imported into a new GitLab instance.
+then imported into a new GitLab instance. You can also:
+
+- [Migrate groups](../../group/import/index.md) using the preferred method.
+- [Migrate groups using file exports](../../group/settings/import_export.md).
## Set up project import/export
@@ -34,8 +37,8 @@ Before you can import a project, you must export it.
Prerequisites:
-- Review the list of [data that will be exported](#items-that-are-exported).
- Not all data is exported.
+- Review the list of [items that are exported](#items-that-are-exported).
+ Not all items are exported.
- You must have at least the Maintainer role for the project.
To export a project and its data, follow these steps:
diff --git a/lib/gitlab/ci/pipeline/chain/validate/external.rb b/lib/gitlab/ci/pipeline/chain/validate/external.rb
index 85bd5f0a7c1..8177502be1d 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/external.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/external.rb
@@ -83,7 +83,9 @@ module Gitlab
project: {
id: project.id,
path: project.full_path,
- created_at: project.created_at&.iso8601
+ created_at: project.created_at&.iso8601,
+ shared_runners_enabled: project.shared_runners_enabled?,
+ group_runners_enabled: project.group_runners_enabled?
},
user: {
id: current_user.id,
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 4da112bc5a0..6d3e954f449 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -63,6 +63,8 @@ module Gitlab
end
def build_mail
+ # See https://github.com/mikel/mail/blob/641060598f8f4be14d79bad8d703e9f2967e1cdb/spec/mail/message_spec.rb#L569
+ # for mail structure
Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError,
Encoding::InvalidByteSequenceError => e
diff --git a/lib/gitlab/service_desk_email.rb b/lib/gitlab/service_desk_email.rb
index 52da10eff3e..14f07140825 100644
--- a/lib/gitlab/service_desk_email.rb
+++ b/lib/gitlab/service_desk_email.rb
@@ -23,6 +23,12 @@ module Gitlab
config.address.sub(Gitlab::IncomingEmail::WILDCARD_PLACEHOLDER, key)
end
+
+ def key_from_fallback_message_id(mail_id)
+ message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/
+
+ mail_id[message_id_regexp, 1]
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6a7621557f2..04122a57738 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -7173,6 +7173,9 @@ msgstr ""
msgid "Cannot assign a confidential epic to a non-confidential issue. Make the issue confidential and try again"
msgstr ""
+msgid "Cannot assign an issue that does not belong under the same group (or descendant) as the epic."
+msgstr ""
+
msgid "Cannot be merged automatically"
msgstr ""
@@ -11193,17 +11196,6 @@ msgstr ""
msgid "CycleAnalytics|Show"
msgstr ""
-msgid "CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCount} label"
-msgid_plural "CycleAnalytics|Showing %{subjectFilterText} and %{selectedLabelsCount} labels"
-msgstr[0] ""
-msgstr[1] ""
-
-msgid "CycleAnalytics|Showing data for group '%{groupName}' and %{selectedProjectCount} projects from %{createdAfter} to %{createdBefore}"
-msgstr ""
-
-msgid "CycleAnalytics|Showing data for group '%{groupName}' from %{createdAfter} to %{createdBefore}"
-msgstr ""
-
msgid "CycleAnalytics|Stage time: %{title}"
msgstr ""
@@ -11231,9 +11223,6 @@ msgstr ""
msgid "CycleAnalytics|Total time"
msgstr ""
-msgid "CycleAnalytics|Type of work"
-msgstr ""
-
msgid "CycleAnalytics|group dropdown filter"
msgstr ""
@@ -15019,6 +15008,9 @@ msgstr ""
msgid "Estimated"
msgstr ""
+msgid "Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -26564,9 +26556,15 @@ msgstr ""
msgid "Only Issue ID or merge request ID is required"
msgstr ""
+msgid "Only Issue can be parent of Task."
+msgstr ""
+
msgid "Only Project Members"
msgstr ""
+msgid "Only Task can be assigned as a child in hierarchy."
+msgstr ""
+
msgid "Only active projects show up in the search and on the dashboard."
msgstr ""
@@ -27319,12 +27317,18 @@ msgstr ""
msgid "Parent"
msgstr ""
+msgid "Parent already has maximum number of children."
+msgstr ""
+
msgid "Parent epic doesn't exist."
msgstr ""
msgid "Parent epic is not present."
msgstr ""
+msgid "Parent must be in the same project as child."
+msgstr ""
+
msgid "Parsing error for param :embed_json. %{message}"
msgstr ""
@@ -41628,6 +41632,11 @@ msgstr ""
msgid "ValueStreamAnalytics|%{stageCount}+ items"
msgstr ""
+msgid "ValueStreamAnalytics|%{subjectFilterText} and %{selectedLabelsCount} label"
+msgid_plural "ValueStreamAnalytics|%{subjectFilterText} and %{selectedLabelsCount} labels"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "ValueStreamAnalytics|%{value}M"
msgstr ""
@@ -41682,13 +41691,22 @@ msgstr ""
msgid "ValueStreamAnalytics|Percentage of deployments that cause an incident in production."
msgstr ""
+msgid "ValueStreamAnalytics|Shows %{selectedFiltersDescription} for group '%{groupName}' and %{selectedProjectCount} projects from %{createdAfter} to %{createdBefore}"
+msgstr ""
+
+msgid "ValueStreamAnalytics|Shows %{selectedFiltersDescription} for group '%{groupName}' from %{createdAfter} to %{createdBefore}"
+msgstr ""
+
+msgid "ValueStreamAnalytics|Tasks by type"
+msgstr ""
+
msgid "ValueStreamAnalytics|There was an error while fetching value stream analytics %{requestTypeName} data."
msgstr ""
msgid "ValueStreamAnalytics|Total number of deploys to production."
msgstr ""
-msgid "ValueStreamAnalytics|Value Stream"
+msgid "ValueStreamAnalytics|Value stream"
msgstr ""
msgid "ValueStreamEvent|Items in stage"
@@ -44175,8 +44193,10 @@ msgstr ""
msgid "Your subscription expired!"
msgstr ""
-msgid "Your subscription has %{remaining_seats_count} out of %{total_seats_count} seats remaining. Even if you reach the number of seats in your subscription, you can continue to add users, and GitLab will bill you for the overage."
-msgstr ""
+msgid "Your subscription has %{remaining_seat_count} out of %{total_seat_count} seat remaining."
+msgid_plural "Your subscription has %{remaining_seat_count} out of %{total_seat_count} seats remaining."
+msgstr[0] ""
+msgstr[1] ""
msgid "Your subscription is now expired. To renew, export your license usage file and email it to %{renewal_service_email}. A new license will be emailed to the email address registered in the %{customers_dot}. You can add this license to your instance. To use Free tier, remove your current license."
msgstr ""
diff --git a/package.json b/package.json
index 5ba42d4abba..87651584a25 100644
--- a/package.json
+++ b/package.json
@@ -107,7 +107,7 @@
"codesandbox-api": "0.0.23",
"compression-webpack-plugin": "^5.0.2",
"copy-webpack-plugin": "^6.4.1",
- "core-js": "^3.22.7",
+ "core-js": "^3.22.8",
"cron-validator": "^1.1.1",
"cronstrue": "^1.122.0",
"cropper": "^2.3.0",
diff --git a/spec/controllers/registrations/welcome_controller_spec.rb b/spec/controllers/registrations/welcome_controller_spec.rb
index c444875bf74..8a5a8490a23 100644
--- a/spec/controllers/registrations/welcome_controller_spec.rb
+++ b/spec/controllers/registrations/welcome_controller_spec.rb
@@ -31,6 +31,7 @@ RSpec.describe Registrations::WelcomeController do
context 'when role and setup_for_company is set' do
before do
+ stub_feature_flags(about_your_company_registration_flow: false)
user.update!(setup_for_company: false)
sign_in(user)
end
@@ -60,6 +61,10 @@ RSpec.describe Registrations::WelcomeController do
end
describe '#update' do
+ before do
+ stub_feature_flags(about_your_company_registration_flow: false)
+ end
+
subject(:update) do
patch :update, params: { user: { role: 'software_developer', setup_for_company: 'false' } }
end
diff --git a/spec/factories/work_items.rb b/spec/factories/work_items.rb
index 6d9dcac6165..e80aa9cc008 100644
--- a/spec/factories/work_items.rb
+++ b/spec/factories/work_items.rb
@@ -9,5 +9,10 @@ FactoryBot.define do
relative_position { RelativePositioning::START_POSITION }
issue_type { :issue }
association :work_item_type, :default
+
+ trait :task do
+ issue_type { :task }
+ association :work_item_type, :default, :task
+ end
end
end
diff --git a/spec/factories/work_items/parent_links.rb b/spec/factories/work_items/parent_links.rb
new file mode 100644
index 00000000000..886aa449a57
--- /dev/null
+++ b/spec/factories/work_items/parent_links.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :parent_link, class: 'WorkItems::ParentLink' do
+ transient do
+ work_item { nil }
+ work_item_parent { nil }
+ end
+
+ after(:build) do |link, evaluator|
+ link.work_item = evaluator.work_item
+ link.work_item_parent = evaluator.work_item_parent
+
+ unless link.work_item && link.work_item_parent
+ project = link.work_item&.project || link.work_item_parent&.project || create(:project)
+ link.work_item ||= create(:work_item, :task, project: project)
+ link.work_item_parent ||= create(:work_item, project: project)
+ end
+ end
+ end
+end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 3eae4955167..30441dac7b6 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -341,6 +341,7 @@ RSpec.describe 'Signup' do
end
it 'redirects to step 2 of the signup process, sets the role and redirects back' do
+ stub_feature_flags(about_your_company_registration_flow: false)
visit new_user_registration_path
fill_in_signup_form
diff --git a/spec/fixtures/api/schemas/external_validation.json b/spec/fixtures/api/schemas/external_validation.json
index e95909a2922..ddcabd4c61e 100644
--- a/spec/fixtures/api/schemas/external_validation.json
+++ b/spec/fixtures/api/schemas/external_validation.json
@@ -12,12 +12,16 @@
"required": [
"id",
"path",
- "created_at"
+ "created_at",
+ "shared_runners_enabled",
+ "group_runners_enabled"
],
"properties": {
"id": { "type": "integer" },
"path": { "type": "string" },
- "created_at": { "type": ["string", "null"], "format": "date-time" }
+ "created_at": { "type": ["string", "null"], "format": "date-time" },
+ "shared_runners_enabled": { "type": "boolean" },
+ "group_runners_enabled": { "type": "boolean" }
}
},
"user": {
diff --git a/spec/fixtures/emails/service_desk_reference_headers.eml b/spec/fixtures/emails/service_desk_reference_headers.eml
new file mode 100644
index 00000000000..ff10fcdd2f6
--- /dev/null
+++ b/spec/fixtures/emails/service_desk_reference_headers.eml
@@ -0,0 +1,31 @@
+Return-Path:
+Received: from myserver.example.com ([unix socket]) by myserver (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Received: from blabla.google.com (blabla.google.com. [1.1.1.1])
+ by bla.google.com with SMTPS id something.1.1.1.1.1.1.1
+ for
+ (Google Transport Security);
+ Mon, 21 Feb 2022 14:41:58 -0800 (PST)
+References:
+
+Received: from mail.example.com (mail.example.com [IPv6:2607:f8b0:4001:c03::234]) by myserver.example.com (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400
+From: "jake@example.com"
+To: "some_unrelated_email@example.com"
+Subject: Re: Insert hilarious subject line here
+Date: Tue, 26 Nov 2019 14:22:41 +0000
+Message-ID: <7e2296f83dbf4de388cbf5f56f52c11f@EXDAG29-1.EXCHANGE.INT>
+Content-Type: multipart/alternative;
+ boundary="_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_"
+MIME-Version: 1.0
+
+--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+
+
+--_000_7e2296f83dbf4de388cbf5f56f52c11fEXDAG291EXCHANGEINT_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+This message has unhelpful References headers with no key, and no key in the To headers either.
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 21299e2d2f2..6ec39be5d29 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -778,3 +778,15 @@ export const boardListQueryResponse = (issuesCount = 20) => ({
},
},
});
+
+export const epicBoardListQueryResponse = (totalWeight = 5) => ({
+ data: {
+ epicBoardList: {
+ __typename: 'EpicList',
+ id: 'gid://gitlab/Boards::EpicList/3',
+ metadata: {
+ totalWeight,
+ },
+ },
+ },
+});
diff --git a/spec/lib/gitlab/email/service_desk_receiver_spec.rb b/spec/lib/gitlab/email/service_desk_receiver_spec.rb
index 49cbec6fffc..c249a5422ff 100644
--- a/spec/lib/gitlab/email/service_desk_receiver_spec.rb
+++ b/spec/lib/gitlab/email/service_desk_receiver_spec.rb
@@ -53,6 +53,18 @@ RSpec.describe Gitlab::Email::ServiceDeskReceiver do
end
end
+ context 'when the email contains no key in the To header and contains reference header with no key' do
+ let(:email) { fixture_file('emails/service_desk_reference_headers.eml') }
+
+ before do
+ stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
+ end
+
+ it 'sends a rejection email' do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
+
context 'when the email does not contain a valid email address' do
before do
stub_service_desk_email_setting(enabled: true, address: 'other_support+%{key}@example.com')
diff --git a/spec/lib/gitlab/service_desk_email_spec.rb b/spec/lib/gitlab/service_desk_email_spec.rb
index 67a1f07eec6..9847496e361 100644
--- a/spec/lib/gitlab/service_desk_email_spec.rb
+++ b/spec/lib/gitlab/service_desk_email_spec.rb
@@ -78,4 +78,10 @@ RSpec.describe Gitlab::ServiceDeskEmail do
end
end
end
+
+ context 'self.key_from_fallback_message_id' do
+ it 'returns reply key' do
+ expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key')
+ end
+ end
end
diff --git a/spec/models/concerns/as_cte_spec.rb b/spec/models/concerns/as_cte_spec.rb
new file mode 100644
index 00000000000..c92d46ef25f
--- /dev/null
+++ b/spec/models/concerns/as_cte_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe AsCte do
+ let(:klass) do
+ Class.new(ApplicationRecord) do
+ include AsCte
+
+ self.table_name = 'users'
+ end
+ end
+
+ let(:query) { klass.where(id: [1, 2, 3]) }
+ let(:name) { :klass_cte }
+
+ describe '.as_cte' do
+ subject { query.as_cte(name) }
+
+ it { expect(subject).to be_a(Gitlab::SQL::CTE) }
+ it { expect(subject.query).to eq(query) }
+ it { expect(subject.table.name).to eq(name.to_s) }
+
+ context 'with materialized parameter' do
+ subject { query.as_cte(name, materialized: materialized).to_arel.to_sql }
+
+ context 'as true' do
+ let(:materialized) { true }
+
+ it { expect(subject).to match /MATERIALIZE/ }
+ end
+
+ context 'as false' do
+ let(:materialized) { false }
+
+ it { expect(subject).not_to match /MATERIALIZE/ }
+ end
+ end
+ end
+end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 06b6123b092..6c1ceb335f7 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -375,6 +375,14 @@ RSpec.describe Namespace do
context 'linear' do
it_behaves_like 'namespace traversal scopes'
+
+ context 'without inner join ancestors query' do
+ before do
+ stub_feature_flags(use_traversal_ids_for_ancestor_scopes_with_inner_join: false)
+ end
+
+ it_behaves_like 'namespace traversal scopes'
+ end
end
shared_examples 'makes recursive queries' do
diff --git a/spec/models/work_items/parent_link_spec.rb b/spec/models/work_items/parent_link_spec.rb
index a66f6e54734..9516baa7340 100644
--- a/spec/models/work_items/parent_link_spec.rb
+++ b/spec/models/work_items/parent_link_spec.rb
@@ -7,4 +7,60 @@ RSpec.describe WorkItems::ParentLink do
it { is_expected.to belong_to(:work_item) }
it { is_expected.to belong_to(:work_item_parent).class_name('WorkItem') }
end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:work_item) }
+ it { is_expected.to validate_presence_of(:work_item_parent) }
+
+ describe 'hierarchy' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:issue) { build(:work_item, project: project) }
+ let_it_be(:task1) { build(:work_item, :task, project: project) }
+ let_it_be(:task2) { build(:work_item, :task, project: project) }
+
+ it 'is valid if not-task parent has task child' do
+ expect(build(:parent_link, work_item: task1, work_item_parent: issue)).to be_valid
+ end
+
+ it 'is not valid if child is not task' do
+ link = build(:parent_link, work_item: issue)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item]).to include('Only Task can be assigned as a child in hierarchy.')
+ end
+
+ it 'is not valid if parent is task' do
+ link = build(:parent_link, work_item_parent: task1)
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item_parent]).to include('Only Issue can be parent of Task.')
+ end
+
+ it 'is not valid if parent is in other project' do
+ link = build(:parent_link, work_item_parent: task1, work_item: build(:work_item))
+
+ expect(link).not_to be_valid
+ expect(link.errors[:work_item_parent]).to include('Parent must be in the same project as child.')
+ end
+
+ context 'when parent already has maximum number of links' do
+ let_it_be(:link1) { create(:parent_link, work_item_parent: issue, work_item: task1) }
+
+ before do
+ stub_const("#{described_class}::MAX_CHILDREN", 1)
+ end
+
+ it 'is not valid when another link is added' do
+ link2 = build(:parent_link, work_item_parent: issue, work_item: task2)
+
+ expect(link2).not_to be_valid
+ expect(link2.errors[:work_item_parent]).to include('Parent already has maximum number of children.')
+ end
+
+ it 'existing link is still valid' do
+ expect(link1).to be_valid
+ end
+ end
+ end
+ end
end
diff --git a/yarn.lock b/yarn.lock
index 89d0f32870c..0cff0d4993b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3847,10 +3847,10 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
-core-js@^3.22.7:
- version "3.22.7"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.7.tgz#8d6c37f630f6139b8732d10f2c114c3f1d00024f"
- integrity sha512-Jt8SReuDKVNZnZEzyEQT5eK6T2RRCXkfTq7Lo09kpm+fHjgGewSbNjV+Wt4yZMhPDdzz2x1ulI5z/w4nxpBseg==
+core-js@^3.22.8:
+ version "3.22.8"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.22.8.tgz#23f860b1fe60797cc4f704d76c93fea8a2f60631"
+ integrity sha512-UoGQ/cfzGYIuiq6Z7vWL1HfkE9U9IZ4Ub+0XSiJTCzvbZzgPA69oDF2f+lgJ6dFFLEdjW5O6svvoKzXX23xFkA==
core-js@~2.3.0:
version "2.3.0"