Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-25 12:09:05 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-25 12:09:05 +0300
commit012ed4e4f69ab58f9d9b58140865a734fa5a9c88 (patch)
tree5ffb4f87d8b495a89662f0e5218c9dd3f4ae0f38
parent5b9a8005eaf815a0cae80a8482ff3f272d33e042 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/components/pajamas/avatar_component.html.haml12
-rw-r--r--app/components/pajamas/avatar_component.rb65
-rw-r--r--app/controllers/projects/incidents_controller.rb2
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/work_items_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb3
-rw-r--r--app/graphql/mutations/work_items/update.rb2
-rw-r--r--app/graphql/types/work_items/widget_interface.rb19
-rw-r--r--app/graphql/types/work_items/widgets/weight_input_type.rb15
-rw-r--r--app/graphql/types/work_items/widgets/weight_type.rb21
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/issue.rb10
-rw-r--r--app/models/project.rb4
-rw-r--r--app/models/work_item.rb2
-rw-r--r--app/models/work_items/type.rb6
-rw-r--r--app/models/work_items/widgets/weight.rb9
-rw-r--r--app/policies/work_item_policy.rb2
-rw-r--r--app/services/work_items/widgets/base_service.rb6
-rw-r--r--app/services/work_items/widgets/weight_service/update_service.rb15
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--db/migrate/20220720210446_add_start_date_to_issues_table.rb9
-rw-r--r--db/schema_migrations/202207202104461
-rw-r--r--db/structure.sql1
-rw-r--r--doc/administration/repository_checks.md6
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/subscriptions/self_managed/index.md6
-rw-r--r--doc/user/project/issues/issue_weight.md1
-rw-r--r--lib/gitlab/background_task.rb95
-rw-r--r--lib/gitlab/daemon.rb1
-rw-r--r--lib/tasks/gitlab/db/lock_writes.rake2
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/components/pajamas/avatar_component_spec.rb135
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb2
-rw-r--r--spec/lib/gitlab/background_task_spec.rb209
-rw-r--r--spec/models/group_spec.rb7
-rw-r--r--spec/models/issue_spec.rb52
-rw-r--r--spec/models/project_spec.rb62
-rw-r--r--spec/models/work_item_spec.rb7
-rw-r--r--spec/models/work_items/type_spec.rb7
-rw-r--r--spec/policies/work_item_policy_spec.rb21
-rw-r--r--spec/requests/api/graphql/mutations/work_items/update_spec.rb24
-rw-r--r--spec/requests/api/graphql/work_item_spec.rb28
-rw-r--r--spec/services/users/create_service_spec.rb15
-rw-r--r--spec/services/users/update_service_spec.rb2
-rw-r--r--spec/services/work_items/update_service_spec.rb4
-rw-r--r--spec/services/work_items/widgets/weight_service/update_service_spec.rb36
-rw-r--r--spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb34
-rw-r--r--spec/support/shared_examples/models/project_shared_examples.rb35
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb17
-rw-r--r--vendor/project_templates/hugo.tar.gzbin1048450 -> 26755 bytes
52 files changed, 765 insertions, 265 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 0c2ea8e4ad0..c5e5b0e121f 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-f8e688fbf64938cf8563f765c040af39f33e0790
+4da75e5814680fe0d657bb734099527c74b76905
diff --git a/app/components/pajamas/avatar_component.html.haml b/app/components/pajamas/avatar_component.html.haml
new file mode 100644
index 00000000000..502f673fe2c
--- /dev/null
+++ b/app/components/pajamas/avatar_component.html.haml
@@ -0,0 +1,12 @@
+- if src
+ = image_tag src,
+ srcset: srcset,
+ alt: alt,
+ class: avatar_classes,
+ height: @size,
+ width: @size,
+ loading: "lazy",
+ **@avatar_options
+- else
+ %div{ @avatar_options, alt: alt, class: avatar_classes }
+ = initial
diff --git a/app/components/pajamas/avatar_component.rb b/app/components/pajamas/avatar_component.rb
new file mode 100644
index 00000000000..09d4776557d
--- /dev/null
+++ b/app/components/pajamas/avatar_component.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Pajamas
+ class AvatarComponent < Pajamas::Component
+ include Gitlab::Utils::StrongMemoize
+
+ # @param record [User, Project, Group]
+ # @param alt [String] text for the alt tag
+ # @param class [String] custom CSS class(es)
+ # @param size [Integer] size in pixel
+ # @param [Hash] avatar_options
+ def initialize(record, alt: nil, class: "", size: 64, avatar_options: {})
+ @record = record
+ @alt = alt
+ @class = binding.local_variable_get(:class)
+ @size = filter_attribute(size.to_i, SIZE_OPTIONS, default: 64)
+ @avatar_options = avatar_options
+ end
+
+ private
+
+ SIZE_OPTIONS = [16, 24, 32, 48, 64, 96].freeze
+
+ def avatar_classes
+ classes = ["gl-avatar", "gl-avatar-s#{@size}", @class]
+ classes.push("gl-avatar-circle") if @record.is_a?(User)
+
+ unless src
+ classes.push("gl-avatar-identicon")
+ classes.push("gl-avatar-identicon-bg#{((@record.id || 0) % 7) + 1}")
+ end
+
+ classes.join(' ')
+ end
+
+ def src
+ strong_memoize(:src) do
+ if @record.is_a?(User)
+ # Users show a gravatar instead of an identicon. Also avatars of
+ # blocked users are only shown if the current_user is an admin.
+ # To not duplicate this logic, we are using existing helpers here.
+ current_user = helpers.current_user rescue nil
+ helpers.avatar_icon_for_user(@record, @size, current_user: current_user)
+ elsif @record.try(:avatar_url)
+ "#{@record.avatar_url}?width=#{@size}"
+ end
+ end
+ end
+
+ def srcset
+ return unless src
+
+ retina_src = src.gsub(/(?<=width=)#{@size}+/, (@size * 2).to_s)
+ "#{src} 1x, #{retina_src} 2x"
+ end
+
+ def alt
+ @alt || @record.name
+ end
+
+ def initial
+ @record.name[0, 1].upcase
+ end
+ end
+end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index f9fa8046962..36b52533e78 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -9,7 +9,7 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:incident_timeline, @project)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:work_items_mvc_2)
+ push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:work_items_hierarchy, @project)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 48f883bcd4b..9cafd442913 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -51,7 +51,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:issue_assignees_widget, project)
push_frontend_feature_flag(:realtime_labels, project)
- push_frontend_feature_flag(:work_items_mvc_2)
+ push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:work_items_hierarchy, project)
end
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index ba23af41bb0..b794785f285 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -3,7 +3,7 @@
class Projects::WorkItemsController < Projects::ApplicationController
before_action do
push_force_frontend_feature_flag(:work_items, project&.work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:work_items_mvc_2)
+ push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:work_items_hierarchy, project)
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 37e472050a0..4de46c9579e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -41,7 +41,7 @@ class ProjectsController < Projects::ApplicationController
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
- push_frontend_feature_flag(:work_items_mvc_2)
+ push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:package_registry_access_level)
push_frontend_feature_flag(:work_items_hierarchy, @project)
end
diff --git a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
index cbe1cfb4099..c50f1ce006e 100644
--- a/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
+++ b/app/graphql/mutations/concerns/mutations/work_items/update_arguments.rb
@@ -18,9 +18,6 @@ module Mutations
argument :description_widget, ::Types::WorkItems::Widgets::DescriptionInputType,
required: false,
description: 'Input for description widget.'
- argument :weight_widget, ::Types::WorkItems::Widgets::WeightInputType,
- required: false,
- description: 'Input for weight widget.'
argument :hierarchy_widget, ::Types::WorkItems::Widgets::HierarchyUpdateInputType,
required: false,
description: 'Input for hierarchy widget.'
diff --git a/app/graphql/mutations/work_items/update.rb b/app/graphql/mutations/work_items/update.rb
index 5d8c574877a..b4ed0a1a3ca 100644
--- a/app/graphql/mutations/work_items/update.rb
+++ b/app/graphql/mutations/work_items/update.rb
@@ -51,3 +51,5 @@ module Mutations
end
end
end
+
+Mutations::WorkItems::Update.prepend_mod
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
index 1b752393296..3f4758a6334 100644
--- a/app/graphql/types/work_items/widget_interface.rb
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -10,6 +10,16 @@ module Types
field :type, ::Types::WorkItems::WidgetTypeEnum, null: true,
description: 'Widget type.'
+ ORPHAN_TYPES = [
+ ::Types::WorkItems::Widgets::DescriptionType,
+ ::Types::WorkItems::Widgets::HierarchyType,
+ ::Types::WorkItems::Widgets::AssigneesType
+ ].freeze
+
+ def self.ce_orphan_types
+ ORPHAN_TYPES
+ end
+
def self.resolve_type(object, context)
case object
when ::WorkItems::Widgets::Description
@@ -18,17 +28,14 @@ module Types
::Types::WorkItems::Widgets::HierarchyType
when ::WorkItems::Widgets::Assignees
::Types::WorkItems::Widgets::AssigneesType
- when ::WorkItems::Widgets::Weight
- ::Types::WorkItems::Widgets::WeightType
else
raise "Unknown GraphQL type for widget #{object}"
end
end
- orphan_types ::Types::WorkItems::Widgets::DescriptionType,
- ::Types::WorkItems::Widgets::HierarchyType,
- ::Types::WorkItems::Widgets::AssigneesType,
- ::Types::WorkItems::Widgets::WeightType
+ orphan_types(*ORPHAN_TYPES)
end
end
end
+
+Types::WorkItems::WidgetInterface.prepend_mod
diff --git a/app/graphql/types/work_items/widgets/weight_input_type.rb b/app/graphql/types/work_items/widgets/weight_input_type.rb
deleted file mode 100644
index a01c63222a5..00000000000
--- a/app/graphql/types/work_items/widgets/weight_input_type.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module WorkItems
- module Widgets
- class WeightInputType < BaseInputObject
- graphql_name 'WorkItemWidgetWeightInput'
-
- argument :weight, GraphQL::Types::Int,
- required: true,
- description: 'Weight of the work item.'
- end
- end
- end
-end
diff --git a/app/graphql/types/work_items/widgets/weight_type.rb b/app/graphql/types/work_items/widgets/weight_type.rb
deleted file mode 100644
index c8eaf560268..00000000000
--- a/app/graphql/types/work_items/widgets/weight_type.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- module WorkItems
- module Widgets
- # Disabling widget level authorization as it might be too granular
- # and we already authorize the parent work item
- # rubocop:disable Graphql/AuthorizeTypes
- class WeightType < BaseObject
- graphql_name 'WorkItemWidgetWeight'
- description 'Represents a weight widget'
-
- implements Types::WorkItems::WidgetInterface
-
- field :weight, GraphQL::Types::Int, null: true,
- description: 'Weight of the work item.'
- end
- # rubocop:enable Graphql/AuthorizeTypes
- end
- end
-end
diff --git a/app/models/group.rb b/app/models/group.rb
index 6d8f8bd7613..122fb48a34b 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -855,6 +855,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end
+ def work_items_mvc_2_feature_flag_enabled?
+ feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2)
+ end
+
# Check for enabled features, similar to `Project#feature_available?`
# NOTE: We still want to keep this after removing `Namespace#feature_available?`.
override :feature_available?
diff --git a/app/models/issue.rb b/app/models/issue.rb
index cae42115bef..8a44d7c3c9b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -100,6 +100,8 @@ class Issue < ApplicationRecord
validates :issue_type, presence: true
validates :namespace, presence: true, if: -> { project.present? }
+ validate :due_date_after_start_date
+
enum issue_type: WorkItems::Type.base_types
alias_method :issuing_parent, :project
@@ -660,6 +662,14 @@ class Issue < ApplicationRecord
private
+ def due_date_after_start_date
+ return unless start_date.present? && due_date.present?
+
+ if due_date < start_date
+ errors.add(:due_date, 'must be greater than or equal to start date')
+ end
+ end
+
override :persist_pg_full_text_search_vector
def persist_pg_full_text_search_vector(search_vector)
Issues::SearchData.upsert({ project_id: project_id, issue_id: id, search_vector: search_vector }, unique_by: %i(project_id issue_id))
diff --git a/app/models/project.rb b/app/models/project.rb
index 46e25564eab..c8d7afdd46f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2983,6 +2983,10 @@ class Project < ApplicationRecord
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end
+ def work_items_mvc_2_feature_flag_enabled?
+ group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2)
+ end
+
def enqueue_record_project_target_platforms
return unless Gitlab.com?
return unless Feature.enabled?(:record_projects_target_platforms, self)
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index d29df0c31fc..965851d431a 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -40,3 +40,5 @@ class WorkItem < Issue
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
end
end
+
+WorkItem.prepend_mod
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index e38d0ae153a..7c4da00479c 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -21,11 +21,11 @@ module WorkItems
}.freeze
WIDGETS_FOR_TYPE = {
- issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight],
+ issue: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy],
incident: [Widgets::Description, Widgets::Hierarchy],
test_case: [Widgets::Description],
requirement: [Widgets::Description],
- task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy, Widgets::Weight]
+ task: [Widgets::Assignees, Widgets::Description, Widgets::Hierarchy]
}.freeze
cache_markdown_field :description, pipeline: :single_line
@@ -83,3 +83,5 @@ module WorkItems
end
end
end
+
+WorkItems::Type.prepend_mod
diff --git a/app/models/work_items/widgets/weight.rb b/app/models/work_items/widgets/weight.rb
deleted file mode 100644
index f589378f307..00000000000
--- a/app/models/work_items/widgets/weight.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module WorkItems
- module Widgets
- class Weight < Base
- delegate :weight, to: :work_item
- end
- end
-end
diff --git a/app/policies/work_item_policy.rb b/app/policies/work_item_policy.rb
index 2f3561f1135..2fd84761a9b 100644
--- a/app/policies/work_item_policy.rb
+++ b/app/policies/work_item_policy.rb
@@ -3,6 +3,8 @@
class WorkItemPolicy < IssuePolicy
condition(:is_member_and_author) { is_project_member? & is_author? }
+ rule { can?(:admin_issue) }.enable :admin_work_item
+
rule { can?(:destroy_issue) | is_member_and_author }.enable :delete_work_item
rule { can?(:update_issue) }.enable :update_work_item
diff --git a/app/services/work_items/widgets/base_service.rb b/app/services/work_items/widgets/base_service.rb
index 037733bbed5..c695651361e 100644
--- a/app/services/work_items/widgets/base_service.rb
+++ b/app/services/work_items/widgets/base_service.rb
@@ -11,6 +11,12 @@ module WorkItems
@widget = widget
@current_user = current_user
end
+
+ private
+
+ def can_admin_work_item?
+ can?(current_user, :admin_work_item, widget.work_item)
+ end
end
end
end
diff --git a/app/services/work_items/widgets/weight_service/update_service.rb b/app/services/work_items/widgets/weight_service/update_service.rb
deleted file mode 100644
index cd62a25358f..00000000000
--- a/app/services/work_items/widgets/weight_service/update_service.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module WorkItems
- module Widgets
- module WeightService
- class UpdateService < WorkItems::Widgets::BaseService
- def update(params: {})
- return unless params.present? && params[:weight]
-
- widget.work_item.weight = params[:weight]
- end
- end
- end
- end
-end
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 361beda4d02..e2f7a88569a 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -51,7 +51,7 @@
.profile-header{ class: [('with-no-profile-tabs' if profile_tabs.empty?)] }
.avatar-holder
= link_to avatar_icon_for_user(@user, 400, current_user: current_user), target: '_blank', rel: 'noopener noreferrer' do
- = image_tag avatar_icon_for_user(@user, 90, current_user: current_user), class: "avatar s90", alt: '', itemprop: 'image'
+ = render Pajamas::AvatarComponent.new(@user, alt: "", size: 96, avatar_options: { itemprop: "image" })
- if @user.blocked? || !@user.confirmed?
.user-info
diff --git a/db/migrate/20220720210446_add_start_date_to_issues_table.rb b/db/migrate/20220720210446_add_start_date_to_issues_table.rb
new file mode 100644
index 00000000000..315d80fa654
--- /dev/null
+++ b/db/migrate/20220720210446_add_start_date_to_issues_table.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddStartDateToIssuesTable < Gitlab::Database::Migration[2.0]
+ enable_lock_retries!
+
+ def change
+ add_column :issues, :start_date, :date
+ end
+end
diff --git a/db/schema_migrations/20220720210446 b/db/schema_migrations/20220720210446
new file mode 100644
index 00000000000..143d05d70f3
--- /dev/null
+++ b/db/schema_migrations/20220720210446
@@ -0,0 +1 @@
+d9ce6e056d66e6c1fb9dc6ac6340cc74cf2572edefce1a2a2cefe0556ee5db41 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index fa24855931e..b0feeea8e00 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16594,6 +16594,7 @@ CREATE TABLE issues (
upvotes_count integer DEFAULT 0 NOT NULL,
work_item_type_id bigint,
namespace_id bigint,
+ start_date date,
CONSTRAINT check_fba63f706d CHECK ((lock_version IS NOT NULL))
);
diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md
index 8cfefdb9b56..b83947b224b 100644
--- a/doc/administration/repository_checks.md
+++ b/doc/administration/repository_checks.md
@@ -52,6 +52,7 @@ disk at:
- `/var/log/gitlab/gitlab-rails` for Omnibus GitLab installations.
- `/home/git/gitlab/log` for installations from source.
+- `/var/log/gitlab` in the Sidekiq pod for GitLab Helm chart installations.
If periodic repository checks cause false alarms, you can clear all repository check states:
@@ -65,8 +66,9 @@ If periodic repository checks cause false alarms, you can clear all repository c
You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories
on [Gitaly servers](gitaly/index.md). To locate the repositories:
-1. Go to the storage location for repositories. For Omnibus GitLab installations, repositories are
- stored by default in the `/var/opt/gitlab/git-data/repositories` directory.
+1. Go to the storage location for repositories:
+ - For Omnibus GitLab installations, repositories are stored in the `/var/opt/gitlab/git-data/repositories` directory by default.
+ - For GitLab Helm chart installations, repositories are stored in the `/home/git/repositories` directory inside the Gitaly pod by default.
1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path)
that you need to check.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index fa317490c56..72bfbe41518 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -22183,7 +22183,6 @@ A time-frame defined as a closed inclusive range of two dates.
| <a id="workitemupdatedtaskinputid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
| <a id="workitemupdatedtaskinputstateevent"></a>`stateEvent` | [`WorkItemStateEvent`](#workitemstateevent) | Close or reopen a work item. |
| <a id="workitemupdatedtaskinputtitle"></a>`title` | [`String`](#string) | Title of the work item. |
-| <a id="workitemupdatedtaskinputweightwidget"></a>`weightWidget` | [`WorkItemWidgetWeightInput`](#workitemwidgetweightinput) | Input for weight widget. |
### `WorkItemWidgetDescriptionInput`
diff --git a/doc/subscriptions/self_managed/index.md b/doc/subscriptions/self_managed/index.md
index c1a11ce7d12..99001cf7451 100644
--- a/doc/subscriptions/self_managed/index.md
+++ b/doc/subscriptions/self_managed/index.md
@@ -255,7 +255,7 @@ It also displays the following information:
| Field | Description |
|:-------------------|:------------|
| Users in License | The number of users you've paid for in the current license loaded on the system. The number does not change unless you [add seats](#add-seats-to-a-subscription) during your current subscription period. |
-| Billable users | The daily count of billable users on your system. The count may change as you block or add users to your instance. |
+| Billable users | The daily count of billable users on your system. The count may change as you block, deactivate, or add users to your instance. |
| Maximum users | The highest number of billable users on your system during the term of the loaded license. |
| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that must be paid before renewal. |
@@ -312,7 +312,7 @@ the contact person who manages your subscription.
It's important to regularly review your user accounts, because:
-- Stale user accounts that are not blocked count as billable users. You may pay more than you should
+- Stale user accounts may count as billable users. You may pay more than you should
if you renew for too many users.
- Stale user accounts can be a security risk. A regular review helps reduce this risk.
@@ -329,7 +329,7 @@ To view the number of _users over license_ go to the **Admin Area**.
You purchase a license for 10 users.
-| Event | Billable members | Maximum users |
+| Event | Billable users | Maximum users |
|:---------------------------------------------------|:-----------------|:--------------|
| Ten users occupy all 10 seats. | 10 | 10 |
| Two new users join. | 12 | 12 |
diff --git a/doc/user/project/issues/issue_weight.md b/doc/user/project/issues/issue_weight.md
index 756fe9699f1..fcc53a239dc 100644
--- a/doc/user/project/issues/issue_weight.md
+++ b/doc/user/project/issues/issue_weight.md
@@ -17,6 +17,7 @@ You can set the weight of an issue during its creation, by changing the
value in the dropdown menu. You can set it to a non-negative integer
value from 0, 1, 2, and so on.
You can remove weight from an issue as well.
+A user with a Reporter role (or above) can set the weight.
This value appears on the right sidebar of an individual issue, as well as
in the issues page next to a weight icon (**{weight}**).
diff --git a/lib/gitlab/background_task.rb b/lib/gitlab/background_task.rb
new file mode 100644
index 00000000000..1f03e32844c
--- /dev/null
+++ b/lib/gitlab/background_task.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+module Gitlab
+ # Used to run small workloads concurrently to other threads in the current process.
+ # This may be necessary when accessing process state, which cannot be done via
+ # Sidekiq jobs.
+ #
+ # Since the given task is put on its own thread, use instances sparingly and only
+ # for fast computations since they will compete with other threads such as Puma
+ # or Sidekiq workers for CPU time and memory.
+ #
+ # Good examples:
+ # - Polling and updating process counters
+ # - Observing process or thread state
+ # - Enforcing process limits at the application level
+ #
+ # Bad examples:
+ # - Running database queries
+ # - Running CPU bound work loads
+ #
+ # As a guideline, aim to yield frequently if tasks execute logic in loops by
+ # making each iteration cheap. If life-cycle callbacks like start and stop
+ # aren't necessary and the task does not loop, consider just using Thread.new.
+ #
+ # rubocop: disable Gitlab/NamespacedClass
+ class BackgroundTask
+ AlreadyStartedError = Class.new(StandardError)
+
+ attr_reader :name
+
+ def running?
+ @state == :running
+ end
+
+ # Possible options:
+ # - name [String] used to identify the task in thread listings and logs (defaults to 'background_task')
+ # - synchronous [Boolean] if true, turns `start` into a blocking call
+ def initialize(task, **options)
+ @task = task
+ @synchronous = options[:synchronous]
+ @name = options[:name] || self.class.name.demodulize.underscore
+ # We use a monitor, not a Mutex, because monitors allow for re-entrant locking.
+ @mutex = ::Monitor.new
+ @state = :idle
+ end
+
+ def start
+ @mutex.synchronize do
+ raise AlreadyStartedError, "background task #{name} already running on #{@thread}" if running?
+
+ start_task = @task.respond_to?(:start) ? @task.start : true
+
+ if start_task
+ @state = :running
+
+ at_exit { stop }
+
+ @thread = Thread.new do
+ Thread.current.name = name
+ @task.call
+ end
+
+ @thread.join if @synchronous
+ end
+ end
+
+ self
+ end
+
+ def stop
+ @mutex.synchronize do
+ break unless running?
+
+ if @thread
+ # If thread is not in a stopped state, interrupt it because it may be sleeping.
+ # This is so we process a stop signal ASAP.
+ @thread.wakeup if @thread.alive?
+ begin
+ # Propagate stop event if supported.
+ @task.stop if @task.respond_to?(:stop)
+
+ # join will rethrow any error raised on the background thread
+ @thread.join unless Thread.current == @thread
+ rescue Exception => ex # rubocop:disable Lint/RescueException
+ Gitlab::ErrorTracking.track_exception(ex, extra: { reported_by: name })
+ end
+ @thread = nil
+ end
+
+ @state = :stopped
+ end
+ end
+ end
+ # rubocop: enable Gitlab/NamespacedClass
+end
diff --git a/lib/gitlab/daemon.rb b/lib/gitlab/daemon.rb
index 04d13778499..49828e54d7e 100644
--- a/lib/gitlab/daemon.rb
+++ b/lib/gitlab/daemon.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
module Gitlab
+ # DEPRECATED. Use Gitlab::BackgroundTask for new code instead.
class Daemon
# Options:
# - recreate: We usually only allow a single instance per process to exist;
diff --git a/lib/tasks/gitlab/db/lock_writes.rake b/lib/tasks/gitlab/db/lock_writes.rake
index 3a083036781..fad949eb718 100644
--- a/lib/tasks/gitlab/db/lock_writes.rake
+++ b/lib/tasks/gitlab/db/lock_writes.rake
@@ -6,7 +6,7 @@ namespace :gitlab do
desc "GitLab | DB | Install prevent write triggers on all databases"
task lock_writes: [:environment, 'gitlab:db:validate_config'] do
- Gitlab::Database::EachDatabase.each_database_connection do |connection, database_name|
+ Gitlab::Database::EachDatabase.each_database_connection(include_shared: false) do |connection, database_name|
create_write_trigger_function(connection)
schemas_for_connection = Gitlab::Database.gitlab_schemas_for_connection(connection)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1b1cd638336..4a906868b5a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -45899,6 +45899,9 @@ msgstr ""
msgid "example.com"
msgstr ""
+msgid "exceeds maximum length (100 usernames)"
+msgstr ""
+
msgid "exceeds the %{max_value_length} character limit"
msgstr ""
diff --git a/spec/components/pajamas/avatar_component_spec.rb b/spec/components/pajamas/avatar_component_spec.rb
new file mode 100644
index 00000000000..3b4e4e49fc2
--- /dev/null
+++ b/spec/components/pajamas/avatar_component_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+require "spec_helper"
+
+RSpec.describe Pajamas::AvatarComponent, type: :component do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:group) { create(:group) }
+
+ let(:options) { {} }
+
+ before do
+ render_inline(described_class.new(record, **options))
+ end
+
+ describe "avatar shape" do
+ context "for a User" do
+ let(:record) { user }
+
+ it "has a circle shape" do
+ expect(page).to have_css ".gl-avatar.gl-avatar-circle"
+ end
+ end
+
+ context "for a Project" do
+ let(:record) { project }
+
+ it "has default shape (rect)" do
+ expect(page).to have_css ".gl-avatar"
+ expect(page).not_to have_css ".gl-avatar-circle"
+ end
+ end
+
+ context "for a Group" do
+ let(:record) { group }
+
+ it "has default shape (rect)" do
+ expect(page).to have_css ".gl-avatar"
+ expect(page).not_to have_css ".gl-avatar-circle"
+ end
+ end
+ end
+
+ describe "avatar image" do
+ context "when it has an uploaded image" do
+ let(:record) { project }
+
+ before do
+ allow(record).to receive(:avatar_url).and_return "/example.png"
+ render_inline(described_class.new(record, **options))
+ end
+
+ it "uses the avatar_url as image src" do
+ expect(page).to have_css "img.gl-avatar[src='/example.png?width=64']"
+ end
+
+ it "uses a srcset for higher resolution on retina displays" do
+ expect(page).to have_css "img.gl-avatar[srcset='/example.png?width=64 1x, /example.png?width=128 2x']"
+ end
+
+ it "uses lazy loading" do
+ expect(page).to have_css "img.gl-avatar[loading='lazy']"
+ end
+
+ context "with size option" do
+ let(:options) { { size: 16 } }
+
+ it "uses that size as param for image src and srcset" do
+ expect(page).to have_css(
+ "img.gl-avatar[src='/example.png?width=16'][srcset='/example.png?width=16 1x, /example.png?width=32 2x']"
+ )
+ end
+ end
+ end
+
+ context "when a project or group has no uploaded image" do
+ let(:record) { project }
+
+ it "uses an identicon with the record's initial" do
+ expect(page).to have_css "div.gl-avatar.gl-avatar-identicon", text: record.name[0].upcase
+ end
+
+ context "when the record has no id" do
+ let(:record) { build :group }
+
+ it "uses an identicon with default background color" do
+ expect(page).to have_css "div.gl-avatar.gl-avatar-identicon-bg1"
+ end
+ end
+ end
+
+ context "when a user has no uploaded image" do
+ let(:record) { user }
+
+ it "uses a gravatar" do
+ expect(rendered_component).to match /gravatar\.com/
+ end
+ end
+ end
+
+ describe "options" do
+ let(:record) { user }
+
+ describe "alt" do
+ context "with a value" do
+ let(:options) { { alt: "Profile picture" } }
+
+ it "uses given value as alt text" do
+ expect(page).to have_css ".gl-avatar[alt='Profile picture']"
+ end
+ end
+
+ context "without a value" do
+ it "uses the record's name as alt text" do
+ expect(page).to have_css ".gl-avatar[alt='#{record.name}']"
+ end
+ end
+ end
+
+ describe "class" do
+ let(:options) { { class: 'gl-m-4' } }
+
+ it 'has the correct custom class' do
+ expect(page).to have_css '.gl-avatar.gl-m-4'
+ end
+ end
+
+ describe "size" do
+ let(:options) { { size: 96 } }
+
+ it 'has the correct size class' do
+ expect(page).to have_css '.gl-avatar.gl-avatar-s96'
+ end
+ end
+ end
+end
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 900cd72c17f..cbd2d30d726 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -17,7 +17,7 @@ RSpec.describe 'User uploads avatar to profile' do
visit user_path(user)
- expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=90"]))
+ expect(page).to have_selector(%Q(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png?width=96"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
diff --git a/spec/lib/gitlab/background_task_spec.rb b/spec/lib/gitlab/background_task_spec.rb
new file mode 100644
index 00000000000..102556b6b2f
--- /dev/null
+++ b/spec/lib/gitlab/background_task_spec.rb
@@ -0,0 +1,209 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+# We need to capture task state from a closure, which requires instance variables.
+# rubocop: disable RSpec/InstanceVariable
+RSpec.describe Gitlab::BackgroundTask do
+ let(:options) { {} }
+ let(:task) do
+ proc do
+ @task_run = true
+ @task_thread = Thread.current
+ end
+ end
+
+ subject(:background_task) { described_class.new(task, **options) }
+
+ def expect_condition
+ Timeout.timeout(3) do
+ sleep 0.1 until yield
+ end
+ end
+
+ context 'when stopped' do
+ it 'is not running' do
+ expect(background_task).not_to be_running
+ end
+
+ describe '#start' do
+ it 'runs the given task on a background thread' do
+ test_thread = Thread.current
+
+ background_task.start
+
+ expect_condition { @task_run == true }
+ expect_condition { @task_thread != test_thread }
+ expect(background_task).to be_running
+ end
+
+ it 'returns self' do
+ expect(background_task.start).to be(background_task)
+ end
+
+ context 'when installing exit handler' do
+ it 'stops a running background task' do
+ expect(background_task).to receive(:at_exit).and_yield
+
+ background_task.start
+
+ expect(background_task).not_to be_running
+ end
+ end
+
+ context 'when task responds to start' do
+ let(:task_class) do
+ Struct.new(:started, :start_retval, :run) do
+ def start
+ self.started = true
+ self.start_retval
+ end
+
+ def call
+ self.run = true
+ end
+ end
+ end
+
+ let(:task) { task_class.new }
+
+ it 'calls start' do
+ background_task.start
+
+ expect_condition { task.started == true }
+ end
+
+ context 'when start returns true' do
+ it 'runs the task' do
+ task.start_retval = true
+
+ background_task.start
+
+ expect_condition { task.run == true }
+ end
+ end
+
+ context 'when start returns false' do
+ it 'does not run the task' do
+ task.start_retval = false
+
+ background_task.start
+
+ expect_condition { task.run.nil? }
+ end
+ end
+ end
+
+ context 'when synchronous is set to true' do
+ let(:options) { { synchronous: true } }
+
+ it 'calls join on the thread' do
+ # Thread has to be run in a block, expect_next_instance_of does not support this.
+ allow_any_instance_of(Thread).to receive(:join) # rubocop:disable RSpec/AnyInstanceOf
+
+ background_task.start
+
+ expect_condition { @task_run == true }
+ expect(@task_thread).to have_received(:join)
+ end
+ end
+ end
+
+ describe '#stop' do
+ it 'is a no-op' do
+ expect { background_task.stop }.not_to change { subject.running? }
+ expect_condition { @task_run.nil? }
+ end
+ end
+ end
+
+ context 'when running' do
+ before do
+ background_task.start
+ end
+
+ describe '#start' do
+ it 'raises an error' do
+ expect { background_task.start }.to raise_error(described_class::AlreadyStartedError)
+ end
+ end
+
+ describe '#stop' do
+ it 'stops running' do
+ expect { background_task.stop }.to change { subject.running? }.from(true).to(false)
+ end
+
+ context 'when task responds to stop' do
+ let(:task_class) do
+ Struct.new(:stopped, :call) do
+ def stop
+ self.stopped = true
+ end
+ end
+ end
+
+ let(:task) { task_class.new }
+
+ it 'calls stop' do
+ background_task.stop
+
+ expect_condition { task.stopped == true }
+ end
+ end
+
+ context 'when task stop raises an error' do
+ let(:error) { RuntimeError.new('task error') }
+ let(:options) { { name: 'test_background_task' } }
+
+ let(:task_class) do
+ Struct.new(:call, :error, keyword_init: true) do
+ def stop
+ raise error
+ end
+ end
+ end
+
+ let(:task) { task_class.new(error: error) }
+
+ it 'stops gracefully' do
+ expect { background_task.stop }.not_to raise_error
+ expect(background_task).not_to be_running
+ end
+
+ it 'reports the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ error, { extra: { reported_by: 'test_background_task' } }
+ )
+
+ background_task.stop
+ end
+ end
+ end
+
+ context 'when task run raises exception' do
+ let(:error) { RuntimeError.new('task error') }
+ let(:options) { { name: 'test_background_task' } }
+ let(:task) do
+ proc do
+ @task_run = true
+ raise error
+ end
+ end
+
+ it 'stops gracefully' do
+ expect_condition { @task_run == true }
+ expect { background_task.stop }.not_to raise_error
+ expect(background_task).not_to be_running
+ end
+
+ it 'reports the error' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
+ error, { extra: { reported_by: 'test_background_task' } }
+ )
+
+ background_task.stop
+ end
+ end
+ end
+end
+# rubocop: enable RSpec/InstanceVariable
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index e8e805b2678..4531ca713f0 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -3383,6 +3383,13 @@ RSpec.describe Group do
end
end
+ describe '#work_items_mvc_2_feature_flag_enabled?' do
+ it_behaves_like 'checks self and root ancestor feature flag' do
+ let(:feature_flag) { :work_items_mvc_2 }
+ let(:feature_flag_method) { :work_items_mvc_2_feature_flag_enabled? }
+ end
+ end
+
describe 'group shares' do
let!(:sub_group) { create(:group, parent: group) }
let!(:sub_sub_group) { create(:group, parent: sub_group) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 89c440dc49c..946d26ef367 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -69,7 +69,57 @@ RSpec.describe Issue do
end
describe 'validations' do
- subject { issue.valid? }
+ subject(:valid?) { issue.valid? }
+
+ describe 'due_date_after_start_date' do
+ let(:today) { Date.today }
+
+ context 'when both values are not present' do
+ let(:issue) { build(:issue) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when start date is present and due date is not' do
+ let(:issue) { build(:work_item, start_date: today) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when due date is present and start date is not' do
+ let(:issue) { build(:work_item, due_date: today) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when both date values are present' do
+ context 'when due date is greater than start date' do
+ let(:issue) { build(:work_item, start_date: today, due_date: 1.week.from_now) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when due date is equal to start date' do
+ let(:issue) { build(:work_item, start_date: today, due_date: today) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when due date is before start date' do
+ let(:issue) { build(:work_item, due_date: today, start_date: 1.week.from_now) }
+
+ it { is_expected.to be_falsey }
+
+ it 'adds an error message' do
+ valid?
+
+ expect(issue.errors.full_messages).to contain_exactly(
+ 'Due date must be greater than or equal to start date'
+ )
+ end
+ end
+ end
+ end
describe 'issue_type' do
let(:issue) { build(:issue, issue_type: issue_type) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2171ee752fd..cef61fbb0a2 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -8239,58 +8239,42 @@ RSpec.describe Project, factory_default: :keep do
end
describe '#work_items_feature_flag_enabled?' do
- shared_examples 'project checking work_items feature flag' do
- context 'when work_items FF is disabled globally' do
- before do
- stub_feature_flags(work_items: false)
- end
+ let_it_be(:group_project) { create(:project, :in_subgroup) }
- it { is_expected.to be_falsey }
- end
+ it_behaves_like 'checks parent group feature flag' do
+ let(:feature_flag_method) { :work_items_feature_flag_enabled? }
+ let(:feature_flag) { :work_items }
+ let(:subject_project) { group_project }
+ end
- context 'when work_items FF is enabled for the project' do
- before do
- stub_feature_flags(work_items: project)
- end
+ context 'when feature flag is enabled for the project' do
+ subject { subject_project.work_items_feature_flag_enabled? }
- it { is_expected.to be_truthy }
+ before do
+ stub_feature_flags(work_items: subject_project)
end
- context 'when work_items FF is enabled globally' do
+ context 'when project belongs to a group' do
+ let(:subject_project) { group_project }
+
it { is_expected.to be_truthy }
end
- end
-
- subject { project.work_items_feature_flag_enabled? }
-
- context 'when a project does not belong to a group' do
- let_it_be(:project) { create(:project, namespace: namespace) }
- it_behaves_like 'project checking work_items feature flag'
- end
-
- context 'when project belongs to a group' do
- let_it_be(:root_group) { create(:group) }
- let_it_be(:group) { create(:group, parent: root_group) }
- let_it_be(:project) { create(:project, group: group) }
-
- it_behaves_like 'project checking work_items feature flag'
-
- context 'when work_items FF is enabled for the root group' do
- before do
- stub_feature_flags(work_items: root_group)
- end
+ context 'when project does not belong to a group' do
+ let(:subject_project) { create(:project, namespace: create(:namespace)) }
it { is_expected.to be_truthy }
end
+ end
+ end
- context 'when work_items FF is enabled for the group' do
- before do
- stub_feature_flags(work_items: group)
- end
+ describe '#work_items_mvc_2_feature_flag_enabled?' do
+ let_it_be(:group_project) { create(:project, :in_subgroup) }
- it { is_expected.to be_truthy }
- end
+ it_behaves_like 'checks parent group feature flag' do
+ let(:feature_flag_method) { :work_items_mvc_2_feature_flag_enabled? }
+ let(:feature_flag) { :work_items_mvc_2 }
+ let(:subject_project) { group_project }
end
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index f33c8e0a186..777ade511b0 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -40,10 +40,9 @@ RSpec.describe WorkItem do
subject { build(:work_item).widgets }
it 'returns instances of supported widgets' do
- is_expected.to match_array([instance_of(WorkItems::Widgets::Description),
- instance_of(WorkItems::Widgets::Hierarchy),
- instance_of(WorkItems::Widgets::Assignees),
- instance_of(WorkItems::Widgets::Weight)])
+ is_expected.to include(instance_of(WorkItems::Widgets::Description),
+ instance_of(WorkItems::Widgets::Hierarchy),
+ instance_of(WorkItems::Widgets::Assignees))
end
end
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index e91617effc0..057bf045f60 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -64,10 +64,9 @@ RSpec.describe WorkItems::Type do
subject { described_class.available_widgets }
it 'returns list of all possible widgets' do
- is_expected.to match_array([::WorkItems::Widgets::Description,
- ::WorkItems::Widgets::Hierarchy,
- ::WorkItems::Widgets::Assignees,
- ::WorkItems::Widgets::Weight])
+ is_expected.to include(::WorkItems::Widgets::Description,
+ ::WorkItems::Widgets::Hierarchy,
+ ::WorkItems::Widgets::Assignees)
end
end
diff --git a/spec/policies/work_item_policy_spec.rb b/spec/policies/work_item_policy_spec.rb
index f8ec7d9f9bc..f3e8bd6a08b 100644
--- a/spec/policies/work_item_policy_spec.rb
+++ b/spec/policies/work_item_policy_spec.rb
@@ -63,6 +63,27 @@ RSpec.describe WorkItemPolicy do
end
end
+ describe 'admin_work_item' do
+ context 'when user is reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_allowed(:admin_work_item) }
+ end
+
+ context 'when user is guest' do
+ let(:current_user) { guest }
+
+ it { is_expected.to be_disallowed(:admin_work_item) }
+
+ context 'when guest authored the work item' do
+ let(:work_item_subject) { authored_work_item }
+ let(:current_user) { guest_author }
+
+ it { is_expected.to be_disallowed(:admin_work_item) }
+ end
+ end
+ end
+
describe 'update_work_item' do
context 'when user is reporter' do
let(:current_user) { reporter }
diff --git a/spec/requests/api/graphql/mutations/work_items/update_spec.rb b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
index 77f7b9bacef..46fd3c2b6c2 100644
--- a/spec/requests/api/graphql/mutations/work_items/update_spec.rb
+++ b/spec/requests/api/graphql/mutations/work_items/update_spec.rb
@@ -128,30 +128,6 @@ RSpec.describe 'Update a work item' do
end
end
- context 'with weight widget input' do
- let(:fields) do
- <<~FIELDS
- workItem {
- widgets {
- type
- ... on WorkItemWidgetWeight {
- weight
- }
- }
- }
- errors
- FIELDS
- end
-
- it_behaves_like 'update work item weight widget' do
- let(:new_weight) { 2 }
-
- let(:input) do
- { 'weightWidget' => { 'weight' => new_weight } }
- end
- end
- end
-
context 'with hierarchy widget input' do
let(:widgets_response) { mutation_response['workItem']['widgets'] }
let(:fields) do
diff --git a/spec/requests/api/graphql/work_item_spec.rb b/spec/requests/api/graphql/work_item_spec.rb
index f17d2ebbb7e..a4e6a3a9791 100644
--- a/spec/requests/api/graphql/work_item_spec.rb
+++ b/spec/requests/api/graphql/work_item_spec.rb
@@ -8,7 +8,7 @@ RSpec.describe 'Query.work_item(id)' do
let_it_be(:developer) { create(:user) }
let_it_be(:guest) { create(:user) }
let_it_be(:project) { create(:project, :private) }
- let_it_be(:work_item) { create(:work_item, project: project, description: '- List item', weight: 1) }
+ let_it_be(:work_item) { create(:work_item, project: project, description: '- List item') }
let_it_be(:child_item1) { create(:work_item, :task, project: project) }
let_it_be(:child_item2) { create(:work_item, :task, confidential: true, project: project) }
let_it_be(:child_link1) { create(:parent_link, work_item_parent: work_item, work_item: child_item1) }
@@ -163,32 +163,6 @@ RSpec.describe 'Query.work_item(id)' do
end
end
- describe 'weight widget' do
- let(:work_item_fields) do
- <<~GRAPHQL
- id
- widgets {
- type
- ... on WorkItemWidgetWeight {
- weight
- }
- }
- GRAPHQL
- end
-
- it 'returns widget information' do
- expect(work_item_data).to include(
- 'id' => work_item.to_gid.to_s,
- 'widgets' => include(
- hash_including(
- 'type' => 'WEIGHT',
- 'weight' => work_item.weight
- )
- )
- )
- end
- end
-
describe 'assignees widget' do
let(:assignees) { create_list(:user, 2) }
let(:work_item) { create(:work_item, project: project, assignees: assignees) }
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index 74340bac055..f3c9701c556 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe Users::CreateService do
describe '#execute' do
+ let(:password) { User.random_password }
let(:admin_user) { create(:admin) }
context 'with an admin user' do
@@ -12,7 +13,7 @@ RSpec.describe Users::CreateService do
context 'when required parameters are provided' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: email, password: 'mydummypass' }
+ { name: 'John Doe', username: 'jduser', email: email, password: password }
end
it 'returns a persisted user' do
@@ -82,13 +83,13 @@ RSpec.describe Users::CreateService do
context 'when force_random_password parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', force_random_password: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, force_random_password: true }
end
it 'generates random password' do
user = service.execute
- expect(user.password).not_to eq 'mydummypass'
+ expect(user.password).not_to eq password
expect(user.password).to be_present
end
end
@@ -99,7 +100,7 @@ RSpec.describe Users::CreateService do
name: 'John Doe',
username: 'jduser',
email: 'jd@example.com',
- password: 'mydummypass',
+ password: password,
password_automatically_set: true
}
end
@@ -121,7 +122,7 @@ RSpec.describe Users::CreateService do
context 'when skip_confirmation parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true }
end
it 'confirms the user' do
@@ -131,7 +132,7 @@ RSpec.describe Users::CreateService do
context 'when reset_password parameter is true' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', reset_password: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, reset_password: true }
end
it 'resets password even if a password parameter is given' do
@@ -152,7 +153,7 @@ RSpec.describe Users::CreateService do
context 'with nil user' do
let(:params) do
- { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass', skip_confirmation: true }
+ { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: password, skip_confirmation: true }
end
let(:service) { described_class.new(nil, params) }
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 52c7b54ed72..411cd7316d8 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
RSpec.describe Users::UpdateService do
- let(:password) { 'longsecret987!' }
+ let(:password) { User.random_password }
let(:user) { create(:user, password: password, password_confirmation: password) }
describe '#execute' do
diff --git a/spec/services/work_items/update_service_spec.rb b/spec/services/work_items/update_service_spec.rb
index 5fee5926823..9db4139c6d5 100644
--- a/spec/services/work_items/update_service_spec.rb
+++ b/spec/services/work_items/update_service_spec.rb
@@ -84,8 +84,7 @@ RSpec.describe WorkItems::UpdateService do
let(:widget_params) do
{
hierarchy_widget: { parent: parent },
- description_widget: { description: 'foo' },
- weight_widget: { weight: 1 }
+ description_widget: { description: 'foo' }
}
end
@@ -104,7 +103,6 @@ RSpec.describe WorkItems::UpdateService do
let(:supported_widgets) do
[
{ klass: WorkItems::Widgets::DescriptionService::UpdateService, callback: :update, params: { description: 'foo' } },
- { klass: WorkItems::Widgets::WeightService::UpdateService, callback: :update, params: { weight: 1 } },
{ klass: WorkItems::Widgets::HierarchyService::UpdateService, callback: :before_update_in_transaction, params: { parent: parent } }
]
end
diff --git a/spec/services/work_items/widgets/weight_service/update_service_spec.rb b/spec/services/work_items/widgets/weight_service/update_service_spec.rb
deleted file mode 100644
index 97e17f1c526..00000000000
--- a/spec/services/work_items/widgets/weight_service/update_service_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe WorkItems::Widgets::WeightService::UpdateService do
- let_it_be(:user) { create(:user) }
- let_it_be(:project) { create(:project) }
- let_it_be_with_reload(:work_item) { create(:work_item, project: project, weight: 1) }
-
- let(:widget) { work_item.widgets.find {|widget| widget.is_a?(WorkItems::Widgets::Weight) } }
-
- describe '#update' do
- subject { described_class.new(widget: widget, current_user: user).update(params: params) } # rubocop:disable Rails/SaveBang
-
- context 'when weight param is present' do
- let(:params) { { weight: 2 } }
-
- it 'correctly sets work item weight value' do
- subject
-
- expect(work_item.weight).to eq(2)
- end
- end
-
- context 'when weight param is not present' do
- let(:params) { {} }
-
- it 'does not change work item weight value', :aggregate_failures do
- expect { subject }
- .to not_change { work_item.weight }
-
- expect(work_item.weight).to eq(1)
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb
deleted file mode 100644
index 3c32b7e0310..00000000000
--- a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-RSpec.shared_examples 'update work item weight widget' do
- it 'updates the weight widget' do
- expect do
- post_graphql_mutation(mutation, current_user: current_user)
- work_item.reload
- end.to change(work_item, :weight).from(nil).to(new_weight)
-
- expect(response).to have_gitlab_http_status(:success)
- expect(mutation_response['workItem']['widgets']).to include(
- {
- 'weight' => new_weight,
- 'type' => 'WEIGHT'
- }
- )
- end
-
- context 'when the updated work item is not valid' do
- it 'returns validation errors without the work item' do
- errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:weight, 'error message') }
-
- allow_next_found_instance_of(::WorkItem) do |instance|
- allow(instance).to receive(:valid?).and_return(false)
- allow(instance).to receive(:errors).and_return(errors)
- end
-
- post_graphql_mutation(mutation, current_user: current_user)
-
- expect(mutation_response['workItem']).to be_nil
- expect(mutation_response['errors']).to match_array(['Weight error message'])
- end
- end
-end
diff --git a/spec/support/shared_examples/models/project_shared_examples.rb b/spec/support/shared_examples/models/project_shared_examples.rb
index 475ac1da04b..0b880f00a22 100644
--- a/spec/support/shared_examples/models/project_shared_examples.rb
+++ b/spec/support/shared_examples/models/project_shared_examples.rb
@@ -25,3 +25,38 @@ RSpec.shared_examples 'returns true if project is inactive' do
end
end
end
+
+RSpec.shared_examples 'checks parent group feature flag' do
+ let(:group) { subject_project.group }
+ let(:root_group) { group.parent }
+
+ subject { subject_project.public_send(feature_flag_method) }
+
+ context 'when feature flag is disabled globally' do
+ before do
+ stub_feature_flags(feature_flag => false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when feature flag is enabled globally' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when feature flag is enabled for the root group' do
+ before do
+ stub_feature_flags(feature_flag => root_group)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when feature flag is enabled for the group' do
+ before do
+ stub_feature_flags(feature_flag => group)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+end
diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
index 034c520887e..341e1a3f56c 100644
--- a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
+++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb
@@ -133,6 +133,23 @@ RSpec.describe 'gitlab:db:lock_writes', :silence_stdout, :reestablished_active_r
end
end
+ context 'multiple shared databases' do
+ before do
+ allow(::Gitlab::Database).to receive(:db_config_share_with).and_return(nil)
+ ci_db_config = Ci::ApplicationRecord.connection_db_config
+ allow(::Gitlab::Database).to receive(:db_config_share_with).with(ci_db_config).and_return('main')
+ end
+
+ it 'does not lock any tables if the ci database is shared with main database' do
+ run_rake_task('gitlab:db:lock_writes')
+
+ expect do
+ ApplicationRecord.connection.execute("delete from ci_builds")
+ Ci::ApplicationRecord.connection.execute("delete from users")
+ end.not_to raise_error
+ end
+ end
+
context 'when unlocking writes' do
before do
run_rake_task('gitlab:db:lock_writes')
diff --git a/vendor/project_templates/hugo.tar.gz b/vendor/project_templates/hugo.tar.gz
index 1f756a696e3..3d037bbf1df 100644
--- a/vendor/project_templates/hugo.tar.gz
+++ b/vendor/project_templates/hugo.tar.gz
Binary files differ