diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index b844e4c5e5bc25d0b31487523d8f38db49ddb87c..ccbe591a63ec70912cd0b69cc2fba15df42b04f3 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -81,9 +81,6 @@ export default {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
- // For Merge Requests
- assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
- // For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
index d1a396182b38b29833450724f2c566b28bac12e0..ce378e24289727840ea6d2f57ee6c394a95b222f 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -74,8 +74,7 @@ export default {
}
if (!this.users.length) {
- const emptyTooltipLabel =
- this.issuableType === 'issue' ? __('Assignee(s)') : __('Assignee');
+ const emptyTooltipLabel = __('Assignee(s)');
names.push(emptyTooltipLabel);
}
@@ -90,6 +89,27 @@ export default {
return counter;
},
+ mergeNotAllowedTooltipMessage() {
+ const assigneesCount = this.users.length;
+
+ if (this.issuableType !== 'merge_request' || assigneesCount === 0) {
+ return null;
+ }
+
+ const cannotMergeCount = this.users.filter(u => u.can_merge === false).length;
+ const canMergeCount = assigneesCount - cannotMergeCount;
+
+ if (canMergeCount === assigneesCount) {
+ // Everyone can merge
+ return null;
+ } else if (cannotMergeCount === assigneesCount && assigneesCount > 1) {
+ return 'No one can merge';
+ } else if (assigneesCount === 1) {
+ return 'Cannot merge';
+ }
+
+ return `${canMergeCount}/${assigneesCount} can merge`;
+ },
},
methods: {
assignSelf() {
@@ -154,6 +174,15 @@ export default {
+
+
+
No assignee
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 86b58c1b1b2a675596873f42f6c8a928c017817e..709940ba6c8189dfe7e625daf5e53ee0783b80bc 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -498,6 +498,16 @@
flex: 1;
}
+ .issuable-meta {
+ .author-link {
+ display: inline-block;
+ }
+
+ .issuable-comments {
+ height: 18px;
+ }
+ }
+
.merge-request-title {
margin-bottom: 2px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 792c618fd4045ef49245423e93227446c7758893..6594f20456d9227ab43c348ea8029c7d29c626e6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1158,6 +1158,8 @@ pre.light-well {
.cannot-be-merged:hover {
color: $red-500;
margin-top: 2px;
+ position: relative;
+ z-index: 2;
}
.private-forks-notice .private-fork-icon {
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index cd9bdabe8d1521eec831a5b74201c9ab6d2d7fc0..8e45baff92a242dc4ed84e3148ec8e0e30d187a8 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -192,12 +192,7 @@ def authorize_update_issuable!
def bulk_update_params
permitted_keys_array = permitted_keys.dup
-
- if resource_name == 'issue'
- permitted_keys_array << { assignee_ids: [] }
- else
- permitted_keys_array.unshift(:assignee_id)
- end
+ permitted_keys_array << { assignee_ids: [] }
params.require(:update).permit(permitted_keys_array)
end
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index 5bf34b83b418d9822c893f0dbf422b4ef93a8364..d17984d32f50cf2e07a7aca33b9d00a461b6ac27 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -190,17 +190,17 @@ def collection_type
end
end
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
def preload_for_collection
+ common_attributes = [:author, :assignees, :labels, :milestone]
@preload_for_collection ||= case collection_type
when 'Issue'
- [:project, :author, :assignees, :labels, :milestone, project: :namespace]
+ common_attributes + [:project, project: :namespace]
when 'MergeRequest'
- [
- :target_project, :author, :assignee, :labels, :milestone,
- source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
- ]
+ common_attributes + [:target_project, source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits]
end
end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
IssuableCollections.prepend(EE::IssuableCollections)
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index 1febc891dcf47844794cbc315089c7fa3e77d02c..cffc29ac0e64e6ab43387b135c272cc1b9303c40 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -20,7 +20,6 @@ def merge_request_params
def merge_request_params_attributes
[
:allow_collaboration,
- :assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
@@ -35,6 +34,7 @@ def merge_request_params_attributes
:title,
:discussion_locked,
label_ids: [],
+ assignee_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 64c88505a161234e6a1d10376395f4ff028824e9..88ec77426d5b3ec3d2ea111ea63a27e7e3c5b83c 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -439,22 +439,6 @@ def sort(items)
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
- def by_assignee(items)
- if filter_by_no_assignee?
- items.where(assignee_id: nil)
- elsif filter_by_any_assignee?
- items.where('assignee_id IS NOT NULL')
- elsif assignee
- items.where(assignee_id: assignee.id)
- elsif assignee_id? || assignee_username? # assignee not found
- items.none
- else
- items
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def filter_by_no_assignee?
# Assignee_id takes precedence over assignee_username
[NONE, FILTER_NONE].include?(params[:assignee_id].to_s.downcase) || params[:assignee_username].to_s == NONE
@@ -478,6 +462,20 @@ def by_author(items)
end
# rubocop: enable CodeReuse/ActiveRecord
+ def by_assignee(items)
+ if filter_by_no_assignee?
+ items.unassigned
+ elsif filter_by_any_assignee?
+ items.assigned
+ elsif assignee
+ items.assigned_to(assignee)
+ elsif assignee_id? || assignee_username? # assignee not found
+ items.none
+ else
+ items
+ end
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def by_milestone(items)
if milestones?
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index aeb3026dd20113db463f87c8e2ab29d79bbf400a..2e99422897cb997bc2db4863bd80a5b8f68296e1 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -144,20 +144,6 @@ def user_cannot_see_confidential_issues?
current_user.blank?
end
-
- def by_assignee(items)
- if filter_by_no_assignee?
- items.unassigned
- elsif filter_by_any_assignee?
- items.assigned
- elsif assignee
- items.assigned_to(assignee)
- elsif assignee_id? || assignee_username? # assignee not found
- items.none
- else
- items
- end
- end
end
IssuesFinder.prepend(EE::IssuesFinder)
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 0ae5d4646bada1e38f8f9269c845c77e92d6fcdb..d19e4bba2bbf571e914482b9942e118cfce1aafe 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -69,7 +69,7 @@ def board_list_data
end
def board_sidebar_user_data
- dropdown_options = issue_assignees_dropdown_options
+ dropdown_options = assignees_dropdown_options('issue')
{
toggle: 'dropdown',
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index dd4c6578d3ebaaed55c4e1a1c6324b8ccb0f6ca5..50baea7c5f98ee82b8f1433c3561a8506781d61e 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -19,8 +19,8 @@ def form_errors(model, type: 'form')
end
end
- def issue_assignees_dropdown_options
- {
+ def assignees_dropdown_options(issuable_type)
+ dropdown_data = {
toggle_class: 'js-user-search js-assignee-search js-multiselect js-save-user-data',
title: 'Select assignee',
filter: true,
@@ -30,8 +30,8 @@ def issue_assignees_dropdown_options
first_user: current_user&.username,
null_user: true,
current_user: true,
- project_id: @project&.id,
- field_name: 'issue[assignee_ids][]',
+ project_id: (@target_project || @project)&.id,
+ field_name: "#{issuable_type}[assignee_ids][]",
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
@@ -41,5 +41,36 @@ def issue_assignees_dropdown_options
current_user_info: UserSerializer.new.represent(current_user)
}
}
+
+ type = issuable_type.to_s
+
+ if type == 'issue' && issue_supports_multiple_assignees? ||
+ type == 'merge_request' && merge_request_supports_multiple_assignees?
+ dropdown_data = multiple_assignees_dropdown_options(dropdown_data)
+ end
+
+ dropdown_data
+ end
+
+ # Overwritten
+ def issue_supports_multiple_assignees?
+ false
+ end
+
+ # Overwritten
+ def merge_request_supports_multiple_assignees?
+ false
+ end
+
+ private
+
+ def multiple_assignees_dropdown_options(options)
+ new_options = options.dup
+
+ new_options[:title] = 'Select assignee(s)'
+ new_options[:data][:'dropdown-header'] = 'Assignee(s)'
+ new_options[:data].delete(:'max-select')
+
+ new_options
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 812163a488587ea3e3a5dc4253549ff6d19406b4..05248be108d68ff24688f4fe135777d86b2e1e22 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -15,11 +15,14 @@ def sidebar_gutter_tooltip_text
sidebar_gutter_collapsed? ? _('Expand sidebar') : _('Collapse sidebar')
end
- def sidebar_assignee_tooltip_label(issuable)
- if issuable.assignee
- issuable.assignee.name
+ def assignees_label(issuable, include_value: true)
+ label = 'Assignee'.pluralize(issuable.assignees.count)
+
+ if include_value
+ sanitized_list = sanitize_name(issuable.assignee_list)
+ "#{label}: #{sanitized_list}"
else
- issuable.allows_multiple_assignees? ? _('Assignee(s)') : _('Assignee')
+ label
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 94743cac37a736a76a54bf3f0141333bd15ded05..e95204a4c17473923a9bdc264713c11afc529fb8 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -24,10 +24,12 @@ def push_to_merge_request_email(recipient_id, merge_request_id, updated_by_user_
end
# rubocop: disable CodeReuse/ActiveRecord
- def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
+ def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id)
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ @previous_assignees = []
+ @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
+
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 02a53853e931d92fabdeb0fe073bcbbdd9d045f3..51642d69073495840e76823cd5055367f9ec758f 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -4,6 +4,7 @@ class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
include EmailsHelper
+ include IssuablesHelper
include Emails::Issues
include Emails::MergeRequests
@@ -24,6 +25,7 @@ class Notify < BaseMailer
helper MembersHelper
helper AvatarsHelper
helper GitlabRoutingHelper
+ helper IssuablesHelper
def test_email(recipient_email, subject, body)
mail(to: recipient_email,
diff --git a/app/models/concerns/deprecated_assignee.rb b/app/models/concerns/deprecated_assignee.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7f12ce39c960acfe8855f8724c05891554b724ff
--- /dev/null
+++ b/app/models/concerns/deprecated_assignee.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+# This module handles backward compatibility for import/export of Merge Requests after
+# multiple assignees feature was introduced. Also, it handles the scenarios where
+# the #26496 background migration hasn't finished yet.
+# Ideally, most of this code should be removed at #59457.
+module DeprecatedAssignee
+ extend ActiveSupport::Concern
+
+ def assignee_ids=(ids)
+ nullify_deprecated_assignee
+ super
+ end
+
+ def assignees=(users)
+ nullify_deprecated_assignee
+ super
+ end
+
+ def assignee_id=(id)
+ self.assignee_ids = Array(id)
+ end
+
+ def assignee=(user)
+ self.assignees = Array(user)
+ end
+
+ def assignee
+ assignees.first
+ end
+
+ def assignee_id
+ assignee_ids.first
+ end
+
+ def assignee_ids
+ if Gitlab::Database.read_only? && pending_assignees_population?
+ return Array(deprecated_assignee_id)
+ end
+
+ update_assignees_relation
+ super
+ end
+
+ def assignees
+ if Gitlab::Database.read_only? && pending_assignees_population?
+ return User.where(id: deprecated_assignee_id)
+ end
+
+ update_assignees_relation
+ super
+ end
+
+ private
+
+ # This will make the background migration process quicker (#26496) as it'll have less
+ # assignee_id rows to look through.
+ def nullify_deprecated_assignee
+ return unless persisted? && Gitlab::Database.read_only?
+
+ update_column(:assignee_id, nil)
+ end
+
+ # This code should be removed in the clean-up phase of the
+ # background migration (#59457).
+ def pending_assignees_population?
+ persisted? && deprecated_assignee_id && merge_request_assignees.empty?
+ end
+
+ # If there's an assignee_id and no relation, it means the background
+ # migration at #26496 didn't reach this merge request yet.
+ # This code should be removed in the clean-up phase of the
+ # background migration (#59457).
+ def update_assignees_relation
+ if pending_assignees_population?
+ transaction do
+ merge_request_assignees.create!(user_id: deprecated_assignee_id, merge_request_id: id)
+ update_column(:assignee_id, nil)
+ end
+ end
+ end
+
+ def deprecated_assignee_id
+ read_attribute(:assignee_id)
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 55acdcc0abb91932bee91d44a4697f279fba6301..9ab2a1647953ccb6b208236d2a86e094d0011d91 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -67,13 +67,6 @@ def award_emojis_loaded?
allow_nil: true,
prefix: true
- delegate :name,
- :email,
- :public_email,
- to: :assignee,
- allow_nil: true,
- prefix: true
-
validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 }
validate :milestone_is_valid
@@ -88,6 +81,19 @@ def award_emojis_loaded?
scope :only_opened, -> { with_state(:opened) }
scope :closed, -> { with_state(:closed) }
+ # rubocop:disable GitlabSecurity/SqlInjection
+ # The `to_ability_name` method is not an user input.
+ scope :assigned, -> do
+ where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
+ end
+ scope :unassigned, -> do
+ where("NOT EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE #{to_ability_name}_id = #{to_ability_name}s.id)")
+ end
+ scope :assigned_to, ->(u) do
+ where("EXISTS (SELECT TRUE FROM #{to_ability_name}_assignees WHERE user_id = ? AND #{to_ability_name}_id = #{to_ability_name}s.id)", u.id)
+ end
+ # rubocop:enable GitlabSecurity/SqlInjection
+
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC') }
scope :order_milestone_due_asc, -> { left_joins_milestones.reorder('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date ASC') }
@@ -104,6 +110,7 @@ def award_emojis_loaded?
participant :author
participant :notes_with_associations
+ participant :assignees
strip_attributes :title
@@ -272,6 +279,10 @@ def parent_class
end
end
+ def assignee_or_author?(user)
+ author_id == user.id || assignees.exists?(user.id)
+ end
+
def today?
Date.today == created_at.to_date
end
@@ -316,11 +327,7 @@ def to_hook_data(user, old_associations: {})
end
if old_assignees != assignees
- if self.is_a?(Issue)
- changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
- else
- changes[:assignee] = [old_assignees&.first&.hook_attrs, assignee&.hook_attrs]
- end
+ changes[:assignees] = [old_assignees.map(&:hook_attrs), assignees.map(&:hook_attrs)]
end
if self.respond_to?(:total_time_spent)
@@ -357,10 +364,18 @@ def to_ability_name
def card_attributes
{
'Author' => author.try(:name),
- 'Assignee' => assignee.try(:name)
+ 'Assignee' => assignee_list
}
end
+ def assignee_list
+ assignees.map(&:name).to_sentence
+ end
+
+ def assignee_username_list
+ assignees.map(&:username).to_sentence
+ end
+
def notes_with_associations
# If A has_many Bs, and B has_many Cs, and you do
# `A.includes(b: :c).each { |a| a.b.includes(:c) }`, sadly ActiveRecord
diff --git a/app/models/issue.rb b/app/models/issue.rb
index de294b03acd3ae410a34c1af1fdaf82f751b4ebd..265d633ff789ad926b1d53cc14bb25842c56a449 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -49,10 +49,6 @@ class Issue < ApplicationRecord
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
- scope :assigned, -> { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
- scope :unassigned, -> { where('NOT EXISTS (SELECT TRUE FROM issue_assignees WHERE issue_id = issues.id)') }
- scope :assigned_to, ->(u) { where('EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = ? AND issue_id = issues.id)', u.id)}
-
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
@@ -75,8 +71,6 @@ class Issue < ApplicationRecord
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
- participant :assignees
-
state_machine :state, initial: :opened do
event :close do
transition [:opened] => :closed
@@ -155,22 +149,6 @@ def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
- # Returns a Hash of attributes to be used for Twitter card metadata
- def card_attributes
- {
- 'Author' => author.try(:name),
- 'Assignee' => assignee_list
- }
- end
-
- def assignee_or_author?(user)
- author_id == user.id || assignees.exists?(user.id)
- end
-
- def assignee_list
- assignees.map(&:name).to_sentence
- end
-
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index cd7347821c2f25f98ae13dbf9d46f63f9311038a..59d249495ee2e96ddf242144b3892e8ac3bd606c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -16,6 +16,7 @@ class MergeRequest < ApplicationRecord
include LabelEventable
include ReactiveCaching
include FromUnion
+ include DeprecatedAssignee
self.reactive_cache_key = ->(model) { [model.project.id, model.iid] }
self.reactive_cache_refresh_interval = 10.minutes
@@ -71,8 +72,7 @@ def merge_request_diff
has_many :suggestions, through: :notes
has_many :merge_request_assignees
- # Will be deprecated at https://gitlab.com/gitlab-org/gitlab-ce/issues/59457
- belongs_to :assignee, class_name: "User"
+ has_many :assignees, class_name: "User", through: :merge_request_assignees
serialize :merge_params, Hash # rubocop:disable Cop/ActiveRecordSerialize
@@ -81,10 +81,6 @@ def merge_request_diff
after_update :reload_diff_if_branch_changed
after_save :ensure_metrics
- # Required until the codebase starts using this relation for single or multiple assignees.
- # TODO: Remove at gitlab-ee#2004 implementation.
- after_save :refresh_merge_request_assignees, if: :assignee_id_changed?
-
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
attr_accessor :allow_broken
@@ -190,19 +186,14 @@ def check_state?(merge_status)
end
scope :join_project, -> { joins(:target_project) }
scope :references_project, -> { references(:target_project) }
- scope :assigned, -> { where("assignee_id IS NOT NULL") }
- scope :unassigned, -> { where("assignee_id IS NULL") }
- scope :assigned_to, ->(u) { where(assignee_id: u.id)}
scope :with_api_entity_associations, -> {
- preload(:author, :assignee, :notes, :labels, :milestone, :timelogs,
+ preload(:assignees, :author, :notes, :labels, :milestone, :timelogs,
latest_merge_request_diff: [:merge_request_diff_commits],
metrics: [:latest_closed_by, :merged_by],
target_project: [:route, { namespace: :route }],
source_project: [:route, { namespace: :route }])
}
- participant :assignee
-
after_save :keep_around_commit
alias_attribute :project, :target_project
@@ -339,31 +330,6 @@ def hook_attrs
Gitlab::HookData::MergeRequestBuilder.new(self).build
end
- # Returns a Hash of attributes to be used for Twitter card metadata
- def card_attributes
- {
- 'Author' => author.try(:name),
- 'Assignee' => assignee.try(:name)
- }
- end
-
- # These method are needed for compatibility with issues to not mess view and other code
- def assignees
- Array(assignee)
- end
-
- def assignee_ids
- Array(assignee_id)
- end
-
- def assignee_ids=(ids)
- write_attribute(:assignee_id, ids.last)
- end
-
- def assignee_or_author?(user)
- author_id == user.id || assignee_id == user.id
- end
-
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -684,15 +650,6 @@ def ensure_merge_request_diff
merge_request_diff || create_merge_request_diff
end
- def refresh_merge_request_assignees
- transaction do
- # Using it instead relation.delete_all in order to avoid adding a
- # dependent: :delete_all (we already have foreign key cascade deletion).
- MergeRequestAssignee.where(merge_request_id: self).delete_all
- merge_request_assignees.create(user_id: assignee_id) if assignee_id
- end
- end
-
def create_merge_request_diff
fetch_ref!
@@ -1210,7 +1167,7 @@ def predefined_variables
variables.append(key: 'CI_MERGE_REQUEST_PROJECT_URL', value: project.web_url)
variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME', value: target_branch.to_s)
variables.append(key: 'CI_MERGE_REQUEST_TITLE', value: title)
- variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee.username) if assignee
+ variables.append(key: 'CI_MERGE_REQUEST_ASSIGNEES', value: assignee_username_list) if assignees.any?
variables.append(key: 'CI_MERGE_REQUEST_MILESTONE', value: milestone.title) if milestone
variables.append(key: 'CI_MERGE_REQUEST_LABELS', value: label_names.join(',')) if labels.present?
variables.concat(source_project_variables)
diff --git a/app/models/project.rb b/app/models/project.rb
index 98f65356fdbc058579b83777c537506d108ed583..7711bf588481f9f615b24e4846b433eff2827c5a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -674,6 +674,10 @@ def first_auto_devops_config
{ scope: :project, status: auto_devops&.enabled || Feature.enabled?(:force_autodevops_on_by_default, self) }
end
+ def multiple_mr_assignees_enabled?
+ Feature.enabled?(:multiple_merge_request_assignees, self)
+ end
+
def daily_statistics_enabled?
Feature.enabled?(:project_daily_statistics, self, default_enabled: true)
end
diff --git a/app/serializers/issuable_sidebar_extras_entity.rb b/app/serializers/issuable_sidebar_extras_entity.rb
index d60253564e1f642f0492f64fd1b74bbb6d36ead6..fb35b7522c51a1b2d027b1d0554a03f27688b8b2 100644
--- a/app/serializers/issuable_sidebar_extras_entity.rb
+++ b/app/serializers/issuable_sidebar_extras_entity.rb
@@ -11,4 +11,6 @@ class IssuableSidebarExtrasEntity < Grape::Entity
expose :subscribed do |issuable|
issuable.subscribed?(request.current_user, issuable.project)
end
+
+ expose :assignees, using: API::Entities::UserBasic
end
diff --git a/app/serializers/issue_sidebar_extras_entity.rb b/app/serializers/issue_sidebar_extras_entity.rb
index 7148cce73ead6062e7b41fcb0fe3d325056d6373..c56d65389d4d1936af19a1fea5aec29230ed6c1a 100644
--- a/app/serializers/issue_sidebar_extras_entity.rb
+++ b/app/serializers/issue_sidebar_extras_entity.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
- expose :assignees, using: API::Entities::UserBasic
end
IssueSidebarExtrasEntity.prepend(EE::IssueSidebarExtrasEntity)
diff --git a/app/serializers/merge_request_assignee_entity.rb b/app/serializers/merge_request_assignee_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6849c62e75963b846db5a356f49f7e7bae022699
--- /dev/null
+++ b/app/serializers/merge_request_assignee_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequestAssigneeEntity < ::API::Entities::UserBasic
+ expose :can_merge do |assignee, options|
+ options[:merge_request]&.can_be_merged_by?(assignee)
+ end
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
index 178e72f4f0a7bd11cc05bde935af99c158c9d409..973e971b4c0a99c951a62fdc15bf3921f661d2c6 100644
--- a/app/serializers/merge_request_basic_entity.rb
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
class MergeRequestBasicEntity < Grape::Entity
- expose :assignee_id
expose :merge_status
expose :merge_error
expose :state
@@ -9,7 +8,7 @@ class MergeRequestBasicEntity < Grape::Entity
expose :rebase_in_progress?, as: :rebase_in_progress
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
- expose :assignee, using: API::Entities::UserBasic
+ expose :assignees, using: API::Entities::UserBasic
expose :task_status, :task_status_short
expose :lock_version, :lock_version
end
diff --git a/app/serializers/merge_request_serializer.rb b/app/serializers/merge_request_serializer.rb
index 4cf84336aa4301cfbbfe1c3ba81ecc9ebecd9339..6f5893516707b2361c76cd6cd1af1ab5685c3b47 100644
--- a/app/serializers/merge_request_serializer.rb
+++ b/app/serializers/merge_request_serializer.rb
@@ -8,9 +8,9 @@ def represent(merge_request, opts = {})
entity =
case opts[:serializer]
when 'sidebar'
- MergeRequestSidebarBasicEntity
+ IssuableSidebarBasicEntity
when 'sidebar_extras'
- IssuableSidebarExtrasEntity
+ MergeRequestSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity
else
diff --git a/app/serializers/merge_request_sidebar_basic_entity.rb b/app/serializers/merge_request_sidebar_basic_entity.rb
deleted file mode 100644
index 0ae7298a7c1de65649cc44c26b179d8d01cae372..0000000000000000000000000000000000000000
--- a/app/serializers/merge_request_sidebar_basic_entity.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-class MergeRequestSidebarBasicEntity < IssuableSidebarBasicEntity
- expose :assignee, if: lambda { |issuable| issuable.assignee } do
- expose :assignee, merge: true, using: API::Entities::UserBasic
-
- expose :can_merge do |issuable|
- issuable.can_be_merged_by?(issuable.assignee)
- end
- end
-end
diff --git a/app/serializers/merge_request_sidebar_extras_entity.rb b/app/serializers/merge_request_sidebar_extras_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7276509c363bcc286418a02b131fb7d8499f4012
--- /dev/null
+++ b/app/serializers/merge_request_sidebar_extras_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
+ expose :assignees do |merge_request|
+ MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 358217f168bc6d7f2b5c110d98f087a981d63fb1..155fc6c4de8f71d70e7ea95bf476474c461af7f6 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -34,14 +34,20 @@ def filter_params(issuable)
end
def filter_assignee(issuable)
- return unless params[:assignee_id].present?
+ return if params[:assignee_ids].blank?
- assignee_id = params[:assignee_id]
+ unless issuable.allows_multiple_assignees?
+ params[:assignee_ids] = params[:assignee_ids].first(1)
+ end
+
+ assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
- if assignee_id.to_s == IssuableFinder::NONE
- params[:assignee_id] = ""
+ if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
+ params[:assignee_ids] = []
+ elsif assignee_ids.any?
+ params[:assignee_ids] = assignee_ids
else
- params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
+ params.delete(:assignee_ids)
end
end
@@ -352,7 +358,7 @@ def associations_before_update(issuable)
end
def has_changes?(issuable, old_labels: [], old_assignees: [])
- valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
+ valid_attrs = [:title, :description, :assignee_ids, :milestone_id, :target_branch]
attrs_changed = valid_attrs.any? do |attr|
issuable.previous_changes.include?(attr.to_s)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index ef08adf4f924c6960ed2525258624618613e7f2c..48ed5afbc2adeee0d94444df9724118a6f1c2fc1 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -20,7 +20,7 @@ def close_service
private
def create_assignee_note(issue, old_assignees)
- SystemNoteService.change_issue_assignees(
+ SystemNoteService.change_issuable_assignees(
issue, issue.project, current_user, old_assignees)
end
@@ -31,26 +31,6 @@ def execute_hooks(issue, action = 'open', old_associations: {})
issue.project.execute_services(issue_data, hooks_scope)
end
- # rubocop: disable CodeReuse/ActiveRecord
- def filter_assignee(issuable)
- return if params[:assignee_ids].blank?
-
- unless issuable.allows_multiple_assignees?
- params[:assignee_ids] = params[:assignee_ids].take(1)
- end
-
- assignee_ids = params[:assignee_ids].select { |assignee_id| assignee_can_read?(issuable, assignee_id) }
-
- if params[:assignee_ids].map(&:to_s) == [IssuableFinder::NONE]
- params[:assignee_ids] = []
- elsif assignee_ids.any?
- params[:assignee_ids] = assignee_ids
- else
- params.delete(:assignee_ids)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def update_project_counter_caches?(issue)
super || issue.confidential_changed?
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 1e7b0bcd7368d3641c14230e7cf33e90ae8ee1a7..31e69a7fff9bc5e27fa3f8be4a1d90dcb998b6a3 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -39,7 +39,7 @@ def handle_changes(issue, options)
if issue.assignees != old_assignees
create_assignee_note(issue, old_assignees)
notification_service.async.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_issue(issue, current_user, old_assignees)
+ todo_service.reassigned_issuable(issue, current_user, old_assignees)
end
if issue.previous_changes.include?('confidential')
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 3da537ae072f22865cf029f15bc2be37503c9cd6..278127b8808fd0c03799c96d0c268c27a7d92417 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -49,9 +49,9 @@ def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics)
end
- def create_assignee_note(merge_request)
- SystemNoteService.change_assignee(
- merge_request, merge_request.project, current_user, merge_request.assignee)
+ def create_assignee_note(merge_request, old_assignees)
+ SystemNoteService.change_issuable_assignees(
+ merge_request, merge_request.project, current_user, old_assignees)
end
def create_pipeline_for(merge_request, user)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 444e4b8a7cf401ab1722e6bc7533cf2202913b60..308e926d95a4f4e94a7516739d8705f202953b41 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -24,13 +24,13 @@ def execute(merge_request)
update_task_event(merge_request) || update(merge_request)
end
- # rubocop:disable Metrics/AbcSize
def handle_changes(merge_request, options)
old_associations = options.fetch(:old_associations, {})
old_labels = old_associations.fetch(:labels, [])
old_mentioned_users = old_associations.fetch(:mentioned_users, [])
+ old_assignees = old_associations.fetch(:assignees, [])
- if has_changes?(merge_request, old_labels: old_labels)
+ if has_changes?(merge_request, old_labels: old_labels, old_assignees: old_assignees)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
end
@@ -45,15 +45,10 @@ def handle_changes(merge_request, options)
merge_request.target_branch)
end
- if merge_request.previous_changes.include?('assignee_id')
- reassigned_merge_request_args = [merge_request, current_user]
-
- old_assignee_id = merge_request.previous_changes['assignee_id'].first
- reassigned_merge_request_args << User.find(old_assignee_id) if old_assignee_id
-
- create_assignee_note(merge_request)
- notification_service.async.reassigned_merge_request(*reassigned_merge_request_args)
- todo_service.reassigned_merge_request(merge_request, current_user)
+ if merge_request.assignees != old_assignees
+ create_assignee_note(merge_request, old_assignees)
+ notification_service.async.reassigned_merge_request(merge_request, current_user, old_assignees)
+ todo_service.reassigned_issuable(merge_request, current_user, old_assignees)
end
if merge_request.previous_changes.include?('target_branch') ||
@@ -81,7 +76,6 @@ def handle_changes(merge_request, options)
)
end
end
- # rubocop:enable Metrics/AbcSize
def handle_task_changes(merge_request)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
index 6891c7458cb248f68de3ea4c748e99bde62848c3..b8942990fd1c031d5a69c89833247e156f879209 100644
--- a/app/services/notification_recipient_service.rb
+++ b/app/services/notification_recipient_service.rb
@@ -247,15 +247,15 @@ class Default < Base
attr_reader :target
attr_reader :current_user
attr_reader :action
- attr_reader :previous_assignee
+ attr_reader :previous_assignees
attr_reader :skip_current_user
- def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
+ def initialize(target, current_user, action:, custom_action: nil, previous_assignees: nil, skip_current_user: true)
@target = target
@current_user = current_user
@action = action
@custom_action = custom_action
- @previous_assignee = previous_assignee
+ @previous_assignees = previous_assignees
@skip_current_user = skip_current_user
end
@@ -270,11 +270,7 @@ def build!
# Re-assign is considered as a mention of the new assignee
case custom_action
- when :reassign_merge_request
- add_recipients(previous_assignee, :mention, nil)
- add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
- when :reassign_issue
- previous_assignees = Array(previous_assignee)
+ when :reassign_merge_request, :reassign_issue
add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end
@@ -287,17 +283,11 @@ def build!
# receive them, too.
add_mentions(current_user, target: target)
- # Add the assigned users, if any
- assignees = case custom_action
- when :new_issue
- target.assignees
- else
- target.assignee
- end
-
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
- add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
+ if target.is_a?(Issuable)
+ add_recipients(target.assignees, :participating, NotificationReason::ASSIGNED)
+ end
add_labels_subscribers
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index f5453ef3a8eb658df8d6f60c01e38b26a6e4eafe..ad85474ab61bfae7c86b2979cd55d98b6aca653e 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -95,8 +95,8 @@ def close_issue(issue, current_user)
# When we reassign an issue we should send an email to:
#
- # * issue old assignee if their notification level is not Disabled
- # * issue new assignee if their notification level is not Disabled
+ # * issue old assignees if their notification level is not Disabled
+ # * issue new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user, previous_assignees = [])
@@ -104,7 +104,7 @@ def reassigned_issue(issue, current_user, previous_assignees = [])
issue,
current_user,
action: "reassign",
- previous_assignee: previous_assignees
+ previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
@@ -140,7 +140,7 @@ def changed_milestone_issue(issue, new_milestone, current_user)
# When create a merge request we should send an email to:
#
# * mr author
- # * mr assignee if their notification level is not Disabled
+ # * mr assignees if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
# * users with custom level checked with "new merge request"
@@ -184,23 +184,25 @@ def new_mentions_in_merge_request(merge_request, new_mentioned_users, current_us
# When we reassign a merge_request we should send an email to:
#
- # * merge_request old assignee if their notification level is not Disabled
- # * merge_request assignee if their notification level is not Disabled
+ # * merge_request old assignees if their notification level is not Disabled
+ # * merge_request new assignees if their notification level is not Disabled
# * users with custom level checked with "reassign merge request"
#
- def reassigned_merge_request(merge_request, current_user, previous_assignee = nil)
+ def reassigned_merge_request(merge_request, current_user, previous_assignees = [])
recipients = NotificationRecipientService.build_recipients(
merge_request,
current_user,
action: "reassign",
- previous_assignee: previous_assignee
+ previous_assignees: previous_assignees
)
+ previous_assignee_ids = previous_assignees.map(&:id)
+
recipients.each do |recipient|
mailer.reassigned_merge_request_email(
recipient.user.id,
merge_request.id,
- previous_assignee&.id,
+ previous_assignee_ids,
current_user.id,
recipient.reason
).deliver_later
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 3baf8c8ad98d9806b6d7d5960a5b71ceea2a1c57..c8c8cb3a3cddb346c08e94ceb7c1cdb089eea31b 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -69,7 +69,7 @@ def change_assignee(noteable, project, author, assignee)
# Called when the assignees of an Issue is changed or removed
#
- # issue - Issue object
+ # issuable - Issuable object (responds to assignees)
# project - Project owning noteable
# author - User performing the change
# assignees - Users being assigned, or nil
@@ -85,9 +85,9 @@ def change_assignee(noteable, project, author, assignee)
# "assigned to @user1 and @user2"
#
# Returns the created Note object
- def change_issue_assignees(issue, project, author, old_assignees)
- unassigned_users = old_assignees - issue.assignees
- added_users = issue.assignees.to_a - old_assignees
+ def change_issuable_assignees(issuable, project, author, old_assignees)
+ unassigned_users = old_assignees - issuable.assignees
+ added_users = issuable.assignees.to_a - old_assignees
text_parts = []
text_parts << "assigned to #{added_users.map(&:to_reference).to_sentence}" if added_users.any?
@@ -95,7 +95,7 @@ def change_issue_assignees(issue, project, author, old_assignees)
body = text_parts.join(' and ')
- create_note(NoteSummary.new(issue, project, author, body, action: 'assignee'))
+ create_note(NoteSummary.new(issuable, project, author, body, action: 'assignee'))
end
# Called when the milestone of a Noteable is changed
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index af4b56029621e508664d7cbbf9880c682d3540a3..bbb42694685e8368ca54b7d1ce4af25695cc48c0 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -49,12 +49,12 @@ def destroy_target(target)
todo_users.each(&:update_todos_count_cache)
end
- # When we reassign an issue we should:
+ # When we reassign an issuable we should:
#
- # * create a pending todo for new assignee if issue is assigned
+ # * create a pending todo for new assignee if issuable is assigned
#
- def reassigned_issue(issue, current_user, old_assignees = [])
- create_assignment_todo(issue, current_user, old_assignees)
+ def reassigned_issuable(issuable, current_user, old_assignees = [])
+ create_assignment_todo(issuable, current_user, old_assignees)
end
# When create a merge request we should:
@@ -82,14 +82,6 @@ def close_merge_request(merge_request, current_user)
mark_pending_todos_as_done(merge_request, current_user)
end
- # When we reassign a merge request we should:
- #
- # * creates a pending todo for new assignee if merge request is assigned
- #
- def reassigned_merge_request(merge_request, current_user)
- create_assignment_todo(merge_request, current_user)
- end
-
# When merge a merge request we should:
#
# * mark all pending todos related to the target for the current user as done
diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4ab40ff265945ffbf32fc5f536e8cecf5a2faf40
--- /dev/null
+++ b/app/views/notify/_reassigned_issuable_email.html.haml
@@ -0,0 +1,10 @@
+%p
+ Assignee changed
+ - if previous_assignees.any?
+ from
+ %strong= sanitize_name(previous_assignees.map(&:name).to_sentence)
+ to
+ - if issuable.assignees.any?
+ %strong= sanitize_name(issuable.assignee_list)
+ - else
+ %strong Unassigned
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index 1094d584a1c0c1e419d3380e21efb472699c070d..6e84f9fb3552070a20cda43d4f0910440650ee48 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/issue_due_email.html.haml b/app/views/notify/issue_due_email.html.haml
index e81144b8fcbe2ae1702f36328f7437929e9694d0..08bc98ca05c1e9aec3c77bf6a2e7af2babf19716 100644
--- a/app/views/notify/issue_due_email.html.haml
+++ b/app/views/notify/issue_due_email.html.haml
@@ -3,7 +3,7 @@
- if @issue.assignees.any?
%p
- Assignee: #{@issue.assignee_list}
+ = assignees_label(@issue)
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
diff --git a/app/views/notify/issue_due_email.text.erb b/app/views/notify/issue_due_email.text.erb
index 3c7a57a8a2e503aa4f936d49dee5a892e25c7dd3..ae50b703fe35c85cbb31fc0b5a05028a59b20375 100644
--- a/app/views/notify/issue_due_email.text.erb
+++ b/app/views/notify/issue_due_email.text.erb
@@ -2,6 +2,6 @@ The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
-Assignee: <%= @issue.assignee_list %>
+<%= assignees_label(@issue) %>
<%= @issue.description %>
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index b9b9e0c3ad720b4ecdc26d9b42e3dfbc7d2993b4..e3b24bbd40543e22c58386e3385ba9272461979c 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/merge_request_unmergeable_email.text.haml b/app/views/notify/merge_request_unmergeable_email.text.haml
index 0c7bf1bb044ddc6cc852ba09232beb16993fe4ed..e9708a297d7e5619d675ee1d815e51e42afad52a 100644
--- a/app/views/notify/merge_request_unmergeable_email.text.haml
+++ b/app/views/notify/merge_request_unmergeable_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 045a43cbc8401a0f4e490bcdc8a7b912c08b0e3e..d623e701a30bd2172ecb39193e31f0e8e6a98e4d 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index e6cdaf85c0d11dc2b10894ef99f5b743f168db64..8aa7939dd0b8cd0c36a6069f5b74a3bfc87157ed 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -4,7 +4,7 @@
- if @issue.assignees.any?
%p
- Assignee: #{@issue.assignee_list}
+ = assignees_label(@issue)
- if @issue.description
%div
diff --git a/app/views/notify/new_issue_email.text.erb b/app/views/notify/new_issue_email.text.erb
index 58a2bcbe5eb195d949181dd2b44b6a829fa20a46..ff258711b4807f36c3df3c753dc78c1f00108b5d 100644
--- a/app/views/notify/new_issue_email.text.erb
+++ b/app/views/notify/new_issue_email.text.erb
@@ -2,6 +2,6 @@ New Issue was created.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
-Assignee: <%= @issue.assignee_list %>
+<%= assignees_label(@issue) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_issue_email.text.erb b/app/views/notify/new_mention_in_issue_email.text.erb
index 173091e4a8058068c24acee93980f9d52e8a7200..8e95063b40fe48bc295569589b9780471b008e2f 100644
--- a/app/views/notify/new_mention_in_issue_email.text.erb
+++ b/app/views/notify/new_mention_in_issue_email.text.erb
@@ -2,6 +2,6 @@ You have been mentioned in an issue.
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= sanitize_name(@issue.author_name) %>
-Assignee: <%= sanitize_name(@issue.assignee_list) %>
+<%= assignees_label(@issue) %>
<%= @issue.description %>
diff --git a/app/views/notify/new_mention_in_merge_request_email.text.erb b/app/views/notify/new_mention_in_merge_request_email.text.erb
index 96a4f3f9eacc903f8d1548a40fc4d4df877d9dfb..3c78e257a88f7c4ab9e9c80959b90f4e564a3550 100644
--- a/app/views/notify/new_mention_in_merge_request_email.text.erb
+++ b/app/views/notify/new_mention_in_merge_request_email.text.erb
@@ -4,6 +4,6 @@ You have been mentioned in Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= sanitize_name(@merge_request.author_name) %>
-Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
+= assignees_label(@merge_request)
<%= @merge_request.description %>
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index db23447dd392a7a97cdff7ac7602baa40ac55b13..77d2e65d285575d74c5497ae4432aaee1613f508 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -5,9 +5,9 @@
%p.details
!= merge_path_description(@merge_request, '→')
-- if @merge_request.assignee_id.present?
+- if @merge_request.assignees.any?
%p
- Assignee: #{sanitize_name(@merge_request.assignee_name)}
+ = assignees_label(@merge_request)
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index 754f4bca1cdb50bea22dafc4ef95499b1fbb88f4..e6c42f1cf5f50e66f5ac8c55362d504af64c2a94 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -4,7 +4,7 @@ New Merge Request <%= @merge_request.to_reference %>
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= @merge_request.author_name %>
-Assignee: <%= @merge_request.assignee_name %>
+<%= assignees_label(@merge_request) %>
<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
<%= @merge_request.description %>
diff --git a/app/views/notify/reassigned_issue_email.html.haml b/app/views/notify/reassigned_issue_email.html.haml
index 6d25488a7e2ae42a9011b7fd8b703b3e04985ca4..6b0889276235ad377b464f8abd0d77e205b9616d 100644
--- a/app/views/notify/reassigned_issue_email.html.haml
+++ b/app/views/notify/reassigned_issue_email.html.haml
@@ -1,10 +1 @@
-%p
- Assignee changed
- - if @previous_assignees.any?
- from
- %strong= sanitize_name(@previous_assignees.map(&:name).to_sentence)
- to
- - if @issue.assignees.any?
- %strong= @issue.assignee_list
- - else
- %strong Unassigned
+= render 'reassigned_issuable_email', issuable: @issue, previous_assignees: @previous_assignees
diff --git a/app/views/notify/reassigned_merge_request_email.html.haml b/app/views/notify/reassigned_merge_request_email.html.haml
index e4f19bc3200d2448e200910051bda256b3c7554f..0aefca6b14a2cf77a46df08945abf95ace9d98d0 100644
--- a/app/views/notify/reassigned_merge_request_email.html.haml
+++ b/app/views/notify/reassigned_merge_request_email.html.haml
@@ -1,10 +1 @@
-%p
- Assignee changed
- - if @previous_assignee
- from
- %strong= sanitize_name(@previous_assignee.name)
- to
- - if @merge_request.assignee_id
- %strong= sanitize_name(@merge_request.assignee_name)
- - else
- %strong Unassigned
+= render 'reassigned_issuable_email', issuable: @merge_request, previous_assignees: @previous_assignees
diff --git a/app/views/notify/reassigned_merge_request_email.text.erb b/app/views/notify/reassigned_merge_request_email.text.erb
index 96c770b521963c54b1fbeaa3eb27a3167907f572..82ec7aa0fa4ea8f8c99abd1ea1cfd9b80c2caf82 100644
--- a/app/views/notify/reassigned_merge_request_email.text.erb
+++ b/app/views/notify/reassigned_merge_request_email.text.erb
@@ -2,5 +2,5 @@ Reassigned Merge Request <%= @merge_request.iid %>
<%= url_for([@merge_request.project.namespace.becomes(Namespace), @merge_request.project, @merge_request, { only_path: false }]) %>
-Assignee changed <%= "from #{sanitize_name(@previous_assignee.name)}" if @previous_assignee -%>
- to <%= "#{@merge_request.assignee_id ? sanitize_name(@merge_request.assignee_name) : 'Unassigned'}" %>
+Assignee changed <%= "from #{sanitize_name(@previous_assignees.map(&:name).to_sentence)}" if @previous_assignees.any? -%>
+ to <%= "#{@merge_request.assignees.any? ? @merge_request.assignee_list : 'Unassigned'}" %>
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index bb20158966e846604af4752e61ed86d4b6a8b958..46087125579b7f7fb62146ca174005d8c1a1bd88 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -52,7 +52,7 @@
CLOSED
- if issue.assignees.any?
%li
- = render 'shared/issuable/assignees', project: @project, issue: issue
+ = render 'shared/issuable/assignees', project: @project, issuable: issue
= render 'shared/issuable_meta_data', issuable: issue
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index b8e0b66e277c2c696d7866a5ee6657ed30dcfb7d..47c8e3d73f579858ad44da5bb5bbceff8431b687 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -53,9 +53,9 @@
%li.issuable-pipeline-broken.d-none.d-sm-inline-block
= link_to merge_request_path(merge_request), class: "has-tooltip", title: _('Cannot be merged automatically') do
= icon('exclamation-triangle')
- - if merge_request.assignee
+ - if merge_request.assignees.any?
%li
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: _('Assigned to :name'))
+ = render 'shared/issuable/assignees', project: merge_request.project, issuable: merge_request
= render_if_exists 'projects/merge_requests/approvals_count', merge_request: merge_request
= render 'shared/issuable_meta_data', issuable: merge_request
diff --git a/app/views/shared/boards/components/sidebar/_assignee.html.haml b/app/views/shared/boards/components/sidebar/_assignee.html.haml
index 1374da9d82cf8ce632ba2ea745ddb49027fec546..af6a519a96748cd1596ec6d78d670341eb740715 100644
--- a/app/views/shared/boards/components/sidebar/_assignee.html.haml
+++ b/app/views/shared/boards/components/sidebar/_assignee.html.haml
@@ -19,7 +19,7 @@
":data-name" => "assignee.name",
":data-username" => "assignee.username" }
.dropdown
- - dropdown_options = issue_assignees_dropdown_options
+ - dropdown_options = assignees_dropdown_options('issue')
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-multiselect.js-save-user-data.js-issue-board-sidebar{ type: 'button', ref: 'assigneeDropdown', data: board_sidebar_user_data,
":data-issuable-id" => "issue.iid" }
= dropdown_options[:title]
diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml
index ef3d44a9241b2881691b4886cc9da320e570d6d9..24734ed66cf7b7f79150e307cbaf9123c60088bf 100644
--- a/app/views/shared/issuable/_assignees.html.haml
+++ b/app/views/shared/issuable/_assignees.html.haml
@@ -1,9 +1,9 @@
- max_render = 4
-- assignees_rendering_overflow = issue.assignees.size > max_render
+- assignees_rendering_overflow = issuable.assignees.size > max_render
- render_count = assignees_rendering_overflow ? max_render - 1 : max_render
-- more_assignees_count = issue.assignees.size - render_count
+- more_assignees_count = issuable.assignees.size - render_count
-- issue.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
+- issuable.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(@project, assignee, name: false, title: "Assigned to :name")
- if more_assignees_count.positive?
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 909eb738f95bf9c07635049a585b16664fab4619..a05a13814aca908ea735db1ec7dc8fb4208ee31a 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -21,10 +21,7 @@
.title
Assignee
.filter-item
- - if type == :issues
- - field_name = "update[assignee_ids][]"
- - else
- - field_name = "update[assignee_id]"
+ - field_name = "update[assignee_ids][]"
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.block
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 1a59055f6528c797ad15f6eb35f67d856f014685..ab01094ed6e4f1581cdd5b9ff212bc64db662baa 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,42 +1,10 @@
- issuable_type = issuable_sidebar[:type]
- signed_in = !!issuable_sidebar.dig(:current_user, :id)
-- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
-- if issuable_type == "issue"
- #js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
- .title.hide-collapsed
- = _('Assignee')
- = icon('spinner spin')
-- else
- - assignee = assignees.first
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body", boundary: 'viewport' }, title: (issuable_sidebar.dig(:assignee, :name) || _('Assignee')) }
- - if issuable_sidebar[:assignee]
- = link_to_member(@project, assignee, size: 24)
- - else
- = icon('user', 'aria-hidden': 'true')
+#js-vue-sidebar-assignees{ data: { field: "#{issuable_type}[assignee_ids]", signed_in: signed_in } }
.title.hide-collapsed
= _('Assignee')
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- - if !signed_in
- %a.gutter-toggle.float-right.js-sidebar-toggle{ role: "button", href: "#", "aria-label" => _('Toggle sidebar') }
- = sidebar_gutter_toggle_icon
- .value.hide-collapsed
- - if issuable_sidebar[:assignee]
- = link_to_member(@project, assignee, size: 32, extra_class: 'bold') do
- - unless issuable_sidebar[:assignee][:can_merge]
- %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: _('Not allowed to merge') }
- = icon('exclamation-triangle', 'aria-hidden': 'true')
- %span.username
- @#{issuable_sidebar[:assignee][:username]}
- - else
- %span.assign-yourself.no-value
- = _('No assignee')
- - if can_edit_issuable
- \-
- %a.js-assign-yourself{ href: '#' }
- = _('assign yourself')
+ = icon('spinner spin')
.selectbox.hide-collapsed
- if assignees.none?
@@ -59,17 +27,15 @@
ability_name: issuable_type,
null_user: true,
display: 'static' } }
- - title = _('Select assignee')
- - if issuable_type == "issue"
- - dropdown_options = issue_assignees_dropdown_options
- - title = dropdown_options[:title]
- - options[:toggle_class] += ' js-multiselect js-save-user-data'
- - data = { field_name: "#{issuable_type}[assignee_ids][]" }
- - data[:multi_select] = true
- - data['dropdown-title'] = title
- - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
- - options[:data].merge!(data)
+ - dropdown_options = assignees_dropdown_options(issuable_type)
+ - title = dropdown_options[:title]
+ - options[:toggle_class] += ' js-multiselect js-save-user-data'
+ - data = { field_name: "#{issuable_type}[assignee_ids][]" }
+ - data[:multi_select] = true
+ - data['dropdown-title'] = title
+ - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
+ - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select']
+ - options[:data].merge!(data)
= dropdown_tag(title, options: options)
diff --git a/app/views/shared/issuable/form/_merge_request_assignee.html.haml b/app/views/shared/issuable/form/_merge_request_assignee.html.haml
deleted file mode 100644
index 05c03dedd9187450124d9c4f44a4541d9f5ebcd0..0000000000000000000000000000000000000000
--- a/app/views/shared/issuable/form/_merge_request_assignee.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-- merge_request = issuable
-.block.assignee
- .sidebar-collapsed-icon.sidebar-collapsed-user{ data: { toggle: "tooltip", placement: "left", container: "body" }, title: sidebar_assignee_tooltip_label(issuable) }
- - if merge_request.assignee
- = link_to_member(@project, merge_request.assignee, size: 24)
- - else
- = icon('user', 'aria-hidden': 'true')
- .title.hide-collapsed
- Assignee
- = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
- - if can_edit_issuable
- = link_to 'Edit', '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
- .value.hide-collapsed
- - if merge_request.assignee
- = link_to_member(@project, merge_request.assignee, size: 32, extra_class: 'bold') do
- - unless merge_request.can_be_merged_by?(merge_request.assignee)
- %span.float-right.cannot-be-merged{ data: { toggle: 'tooltip', placement: 'left' }, title: 'Not allowed to merge' }
- = icon('exclamation-triangle', 'aria-hidden': 'true')
- %span.username
- = merge_request.assignee.to_reference
- - else
- %span.assign-yourself.no-value
- No assignee
- - if can_edit_issuable
- \-
- %a.js-assign-yourself{ href: '#' }
- assign yourself
-
- .selectbox.hide-collapsed
- = f.hidden_field 'assignee_id', value: merge_request.assignee_id, id: 'issue_assignee_id'
- = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: @project&.id, author_id: merge_request.author_id, field_name: 'merge_request[assignee_id]', issue_update: issuable_json_path(merge_request), ability_name: 'merge_request', null_user: true } })
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index e370dff9526b48c7efc6bef0ee6f7db743590e38..1e03440a5dc877918e4253c3b2f15b5864365bbe 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -8,11 +8,8 @@
%hr
.row
%div{ class: (has_due_date ? "col-lg-6" : "col-12") }
- .form-group.row.issue-assignee
- - if issuable.is_a?(Issue)
- = render "shared/issuable/form/metadata_issue_assignee", issuable: issuable, form: form, has_due_date: has_due_date
- - else
- = render "shared/issuable/form/metadata_merge_request_assignee", issuable: issuable, form: form, has_due_date: has_due_date
+ .form-group.row.merge-request-assignee
+ = render "shared/issuable/form/metadata_issuable_assignee", issuable: issuable, form: form, has_due_date: has_due_date
.form-group.row.issue-milestone
= form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
diff --git a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
similarity index 64%
rename from app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
rename to app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index 6d4f9ccd66f6eff9113b21123207f960ba09ddbc..5336159e7621676fcd682971567fad3a5bec2f6d 100644
--- a/app/views/shared/issuable/form/_metadata_issue_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -1,4 +1,4 @@
-= form.label :assignee_ids, "Assignee", class: "col-form-label #{"col-md-2 col-lg-4" if has_due_date}"
+= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
@@ -7,5 +7,5 @@
- if issuable.assignees.length === 0
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
- = dropdown_tag(users_dropdown_label(issuable.assignees), options: issue_assignees_dropdown_options)
- = link_to 'Assign to me', '#', class: "assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
+ = dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
+ = link_to 'Assign to me', '#', class: "assign-to-me-link qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb
index 1952f84ed621a1dbabed454fbadcf5979a2a68c3..43b69470d2c243a8096404bdc11123af1bd78d6a 100644
--- a/db/fixtures/development/10_merge_requests.rb
+++ b/db/fixtures/development/10_merge_requests.rb
@@ -21,7 +21,7 @@
title: FFaker::Lorem.sentence(6),
description: FFaker::Lorem.sentences(3).join(" "),
milestone: project.milestones.sample,
- assignee: project.team.users.sample,
+ assignees: [project.team.users.sample],
label_ids: label_ids
}
diff --git a/ee/app/helpers/ee/form_helper.rb b/ee/app/helpers/ee/form_helper.rb
index b41ac6956001832593a6b27b160e3203a8f3d5b1..ba0ec340d1593a18b7be07d57f6d6288599b1b8d 100644
--- a/ee/app/helpers/ee/form_helper.rb
+++ b/ee/app/helpers/ee/form_helper.rb
@@ -2,16 +2,12 @@
module EE
module FormHelper
- def issue_assignees_dropdown_options
- options = super
-
- if current_board_parent.feature_available?(:multiple_issue_assignees)
- options[:title] = 'Select assignee(s)'
- options[:data][:'dropdown-header'] = 'Assignee(s)'
- options[:data].delete(:'max-select')
- end
+ def issue_supports_multiple_assignees?
+ current_board_parent.feature_available?(:multiple_issue_assignees)
+ end
- options
+ def merge_request_supports_multiple_assignees?
+ @merge_request&.allows_multiple_assignees?
end
end
end
diff --git a/ee/app/models/ee/merge_request.rb b/ee/app/models/ee/merge_request.rb
index 9146834dca09dd0362b8db76e32b5d01e9afaf5a..ec7b27461ba1890a6c15228fa9579b3069a9a515 100644
--- a/ee/app/models/ee/merge_request.rb
+++ b/ee/app/models/ee/merge_request.rb
@@ -54,6 +54,11 @@ def mergeable_ci_state?
super
end
+ def allows_multiple_assignees?
+ project.multiple_mr_assignees_enabled? &&
+ project.feature_available?(:multiple_merge_request_assignees)
+ end
+
def supports_weight?
false
end
diff --git a/ee/app/models/license.rb b/ee/app/models/license.rb
index 8357832b3641e06adfb8dab580dfdd6eb97795f1..53f512c0200b2221ab465f4cf0823b6aae80ad96 100644
--- a/ee/app/models/license.rb
+++ b/ee/app/models/license.rb
@@ -26,6 +26,7 @@ class License < ApplicationRecord
merge_request_approvers
multiple_ldap_servers
multiple_issue_assignees
+ multiple_merge_request_assignees
multiple_project_issue_boards
push_rules
protected_refs_for_users
diff --git a/ee/app/views/notify/add_merge_request_approver_email.html.haml b/ee/app/views/notify/add_merge_request_approver_email.html.haml
index b3ba8e16fae1cfb038e1bb5e47db583625bfb806..7f8a00b64c593457b2e48c8b25c7ff9836c5de6e 100644
--- a/ee/app/views/notify/add_merge_request_approver_email.html.haml
+++ b/ee/app/views/notify/add_merge_request_approver_email.html.haml
@@ -4,9 +4,13 @@
%p.details
!= merge_path_description(@merge_request, '→')
-- if @merge_request.assignee_id.present?
+- if @merge_request.assignees.any?
%p
- Assignee: #{@merge_request.author_name} → #{@merge_request.assignee_name}
+ - label = assignees_label(@merge_request, include_value: false)
+ - author_name = sanitize_name(@merge_request.author_name)
+ - assignee_list = sanitize_name(@merge_request.assignee_list)
+
+ "#{label}: #{author_name} → #{assignee_list}"
= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter
diff --git a/ee/app/views/notify/add_merge_request_approver_email.text.erb b/ee/app/views/notify/add_merge_request_approver_email.text.erb
index 9a49bafdbec9fa77dee91188ec7b934091635315..9bb2a02eebfb5ecc19e8dde9c8bcf0635e24f9f1 100644
--- a/ee/app/views/notify/add_merge_request_approver_email.text.erb
+++ b/ee/app/views/notify/add_merge_request_approver_email.text.erb
@@ -4,5 +4,5 @@
<%= merge_path_description(@merge_request, 'to') %>
Author: <%= sanitize_name(@merge_request.author_name) %>
-Assignee: <%= sanitize_name(@merge_request.assignee_name) %>
+<%= assignees_label(@merge_request) %>
<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %>
diff --git a/ee/app/views/notify/approved_merge_request_email.html.haml b/ee/app/views/notify/approved_merge_request_email.html.haml
index 996f3922063e35af76c1de9af0375c663d73345f..fe19067c03dd513cd2432e56e57d0e846202be7b 100644
--- a/ee/app/views/notify/approved_merge_request_email.html.haml
+++ b/ee/app/views/notify/approved_merge_request_email.html.haml
@@ -41,6 +41,19 @@
padding-right: 10px !important;
}
}
+
+ ul.assignees-list {
+ list-style: none;
+ padding: 0px;
+ display: block;
+ margin-top: 0px;
+ }
+ ul.assignees-list li {
+ display: inline-block;
+ padding-right: 12px;
+ padding-top: 8px;
+ }
+
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
@@ -122,18 +135,17 @@
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
- - if @merge_request.assignee
+ - if @merge_request.assignees.any?
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Assignee
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.assignee, 24, only_path: false), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a.muted{ href: user_url(@merge_request.assignee), style: "color:#333333;text-decoration:none;" }
- = @merge_request.assignee.name
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" }
+ = assignees_label(@merge_request, include_value: false)
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; margin: 0; padding: 14px 0 0px 5px; font-size: 15px; line-height: 1.4; color: #333333; font-weight: 400; width: 75%; border-top-style: solid; border-top-color: #ededed; border-top-width: 1px; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" }
+ %ul.assignees-list{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; padding-right: 5px; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" }
+ - @merge_request.assignees.each do |assignee|
+ %li
+ %img.avatar{ alt: "Avatar", height: "24", src: avatar_icon_for_user(assignee, 24, only_path: false), style: "border-radius: 12px; max-width: 100%; height: auto; -ms-interpolation-mode: bicubic; margin: -2px 0;", width: "24" }/
+ %a.muted{ href: user_url(assignee), style: "color: #333333; text-decoration: none; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; vertical-align: top;" }
+ = assignee.name
-# EE-specific start
= render 'layouts/mailer/additional_text'
diff --git a/ee/app/views/notify/approved_merge_request_email.text.haml b/ee/app/views/notify/approved_merge_request_email.text.haml
index 5f86a244ac05d97b90de4295d6b384c5e692f648..f91ebb139bb48f61d25178cae1d71c8b0c4fd575 100644
--- a/ee/app/views/notify/approved_merge_request_email.text.haml
+++ b/ee/app/views/notify/approved_merge_request_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/ee/app/views/notify/unapproved_merge_request_email.html.haml b/ee/app/views/notify/unapproved_merge_request_email.html.haml
index e7578d7095296e01772f0a2d4f2381902a48d9f2..2d731eb920667cdd5242bd378b31faa9a820847a 100644
--- a/ee/app/views/notify/unapproved_merge_request_email.html.haml
+++ b/ee/app/views/notify/unapproved_merge_request_email.html.haml
@@ -41,6 +41,19 @@
padding-right: 10px !important;
}
}
+
+ ul.assignees-list {
+ list-style: none;
+ padding: 0px;
+ display: block;
+ margin-top: 0px;
+ }
+ ul.assignees-list li {
+ display: inline-block;
+ padding-right: 12px;
+ padding-top: 8px;
+ }
+
%body{ style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;height:100%;font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" }
%table#body{ border: "0", cellpadding: "0", cellspacing: "0", style: "background-color:#fafafa;margin:0;padding:0;text-align:center;min-width:640px;width:100%;" }
%tbody
@@ -121,18 +134,17 @@
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
%a.muted{ href: user_url(@merge_request.author), style: "color:#333333;text-decoration:none;" }
= @merge_request.author.name
- - if @merge_request.assignee
+ - if @merge_request.assignees.any?
%tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" } Assignee
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;color:#333333;font-weight:400;width:75%;padding-left:5px;border-top:1px solid #ededed;" }
- %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" }
- %tbody
- %tr
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;padding-right:5px;" }
- %img.avatar{ height: "24", src: avatar_icon_for_user(@merge_request.assignee, 24), style: "display:block;border-radius:12px;margin:-2px 0;", width: "24", alt: "Avatar" }/
- %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;vertical-align:middle;" }
- %a.muted{ href: user_url(@merge_request.assignee), style: "color:#333333;text-decoration:none;" }
- = @merge_request.assignee.name
+ %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;font-size:15px;line-height:1.4;color:#8c8c8c;font-weight:300;padding:14px 0;margin:0;border-top:1px solid #ededed;" }
+ = assignees_label(@merge_request, include_value: false)
+ %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; margin: 0; padding: 14px 0 0px 5px; font-size: 15px; line-height: 1.4; color: #333333; font-weight: 400; width: 75%; border-top-style: solid; border-top-color: #ededed; border-top-width: 1px; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" }
+ %ul.assignees-list{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; padding-right: 5px; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; mso-table-lspace: 0pt; mso-table-rspace: 0pt;" }
+ - @merge_request.assignees.each do |assignee|
+ %li
+ %img.avatar{ alt: "Avatar", height: "24", src: avatar_icon_for_user(assignee, 24, only_path: false), style: "border-radius: 12px; max-width: 100%; height: auto; -ms-interpolation-mode: bicubic; margin: -2px 0;", width: "24" }/
+ %a.muted{ href: user_url(assignee), style: "color: #333333; text-decoration: none; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; vertical-align: top;" }
+ = assignee.name
-# EE-specific start
= render 'layouts/mailer/additional_text'
diff --git a/ee/app/views/notify/unapproved_merge_request_email.text.haml b/ee/app/views/notify/unapproved_merge_request_email.text.haml
index da3f3a4326e77d3f4bdc1227792f0e2093db9c93..f78dfddcefb36c9f91b7f5ab317828c101e3d3d9 100644
--- a/ee/app/views/notify/unapproved_merge_request_email.text.haml
+++ b/ee/app/views/notify/unapproved_merge_request_email.text.haml
@@ -5,4 +5,4 @@ Merge Request url: #{project_merge_request_url(@merge_request.target_project, @m
= merge_path_description(@merge_request, 'to')
Author: #{sanitize_name(@merge_request.author_name)}
-Assignee: #{sanitize_name(@merge_request.assignee_name)}
+= assignees_label(@merge_request)
diff --git a/ee/changelogs/unreleased/osw-multi-assignees-merge-requests-ee.yml b/ee/changelogs/unreleased/osw-multi-assignees-merge-requests-ee.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ab2c38963a6f2ec0acac59abed8f74a2ed855fc3
--- /dev/null
+++ b/ee/changelogs/unreleased/osw-multi-assignees-merge-requests-ee.yml
@@ -0,0 +1,5 @@
+---
+title: Support multiple assignees for merge requests
+merge_request: 10161
+author:
+type: added
diff --git a/ee/lib/api/github/entities.rb b/ee/lib/api/github/entities.rb
index 10b4e6c2dab72cfd4fff0a5a70a24192839c24be..5f73b578dfc7acfa54e9a160066c86eab4c703d9 100644
--- a/ee/lib/api/github/entities.rb
+++ b/ee/lib/api/github/entities.rb
@@ -135,7 +135,9 @@ class NoteableComment < Grape::Entity
class PullRequest < Grape::Entity
expose :title
- expose :assignee, using: User
+ expose :assignee, using: User do |merge_request|
+ merge_request.assignee
+ end
expose :author, as: :user, using: User
expose :created_at
expose :description, as: :body
diff --git a/ee/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/ee/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
index 2fae615bb1939b534c1edd503d6b7b1c66690361..6fca4c587886089802936582d9f4631aeef6314e 100644
--- a/ee/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
+++ b/ee/spec/controllers/projects/merge_requests/drafts_controller_spec.rb
@@ -17,7 +17,7 @@
before do
sign_in(user)
- stub_licensed_features(batch_comments: true)
+ stub_licensed_features(batch_comments: true, multiple_merge_request_assignees: true)
stub_commonmark_sourcepos_disabled
end
@@ -237,15 +237,17 @@ def update_draft_note(overrides = {})
end
it 'publishes a draft note with quick actions and applies them' do
- create(:draft_note, merge_request: merge_request, author: user, note: "/assign #{user.to_reference}")
+ project.add_developer(user2)
+ create(:draft_note, merge_request: merge_request, author: user,
+ note: "/assign #{user.to_reference} #{user2.to_reference}")
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignees).to be_empty
expect { post :publish, params: params }.to change { Note.count }.by(1)
.and change { DraftNote.count }.by(-1)
expect(response).to have_gitlab_http_status(200)
- expect(merge_request.reload.assignee_id).to eq(user.id)
+ expect(merge_request.reload.assignee_ids).to match_array([user.id, user2.id])
expect(Note.last.system?).to be true
end
diff --git a/ee/spec/features/issues/form_spec.rb b/ee/spec/features/issues/form_spec.rb
index aed05dbf98a68cca9e1feb2e81adef77804d6e95..48ef1eb566a1b903fda2efd07a2110ffe1853588 100644
--- a/ee/spec/features/issues/form_spec.rb
+++ b/ee/spec/features/issues/form_spec.rb
@@ -35,8 +35,8 @@
# the original method, resulting in infinite recurison when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
- original_issue_dropdown_options = EE::FormHelper.instance_method(:issue_assignees_dropdown_options)
- allow_any_instance_of(EE::FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
+ original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options)
+ allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
diff --git a/ee/spec/features/merge_request/user_creates_multiple_assignees_mr_spec.rb b/ee/spec/features/merge_request/user_creates_multiple_assignees_mr_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2668f7ab5243adb67155968d8b63480eb3a3037f
--- /dev/null
+++ b/ee/spec/features/merge_request/user_creates_multiple_assignees_mr_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Merge request > User creates MR with multiple assignees' do
+ include_context 'merge request create context'
+
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: true)
+ end
+
+ it_behaves_like 'multiple assignees merge request', 'creates', 'Submit merge request'
+end
diff --git a/ee/spec/features/merge_request/user_edits_multiple_assignees_mr_spec.rb b/ee/spec/features/merge_request/user_edits_multiple_assignees_mr_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e18d32ff632c245b4f215f3dd8be3b8fc47a536d
--- /dev/null
+++ b/ee/spec/features/merge_request/user_edits_multiple_assignees_mr_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Merge request > User edits MR with multiple assignees' do
+ include_context 'merge request edit context'
+
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: true)
+ end
+
+ it_behaves_like 'multiple assignees merge request', 'updates', 'Save changes'
+end
diff --git a/ee/spec/mailers/notify_spec.rb b/ee/spec/mailers/notify_spec.rb
index 57635b6fbb8def0fd294c4fc0d020a6f33d096a8..8c8a1b48a76234aa2fff1c8a174481cb9cccb301 100644
--- a/ee/spec/mailers/notify_spec.rb
+++ b/ee/spec/mailers/notify_spec.rb
@@ -12,12 +12,13 @@
set(:user) { create(:user) }
set(:current_user) { create(:user, email: "current@email.com") }
set(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') }
+ set(:assignee2) { create(:user, email: 'assignee2@example.com', name: 'Jane Doe') }
set(:merge_request) do
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee, assignee2],
description: 'Awesome description')
end
@@ -85,9 +86,7 @@
end
subject do
- described_class.new_merge_request_email(
- merge_request.assignee_id, merge_request.id
- )
+ described_class.new_merge_request_email(assignee.id, merge_request.id)
end
it "contains the approvers list" do
@@ -100,7 +99,7 @@
subject { described_class.approved_merge_request_email(recipient.id, merge_request.id, last_approver.id) }
before do
- merge_request.approvals.create(user: merge_request.assignee)
+ merge_request.approvals.create(user: assignee)
merge_request.approvals.create(user: last_approver)
end
@@ -130,13 +129,20 @@
end
it 'contains the names of all of the approvers' do
- is_expected.to have_body_text /#{merge_request.assignee.name}/
- is_expected.to have_body_text /#{last_approver.name}/
+ merge_request.approvals.each do |approval|
+ is_expected.to have_body_text /#{approval.user.name}/
+ end
+ end
+
+ it 'contains the names of all assignees' do
+ merge_request.assignees.each do |assignee|
+ is_expected.to have_body_text /#{assignee.name}/
+ end
end
context 'when merge request has no assignee' do
before do
- merge_request.update(assignee: nil)
+ merge_request.update(assignees: [])
end
it 'does not show the assignee' do
@@ -150,7 +156,7 @@
subject { described_class.unapproved_merge_request_email(recipient.id, merge_request.id, last_unapprover.id) }
before do
- merge_request.approvals.create(user: merge_request.assignee)
+ merge_request.approvals.create(user: assignee)
end
it_behaves_like 'a multiple recipients email'
@@ -179,7 +185,15 @@
end
it 'contains the names of all of the approvers' do
- is_expected.to have_body_text /#{merge_request.assignee.name}/
+ merge_request.approvals.each do |approval|
+ is_expected.to have_body_text /#{approval.user.name}/
+ end
+ end
+
+ it 'contains the names of all assignees' do
+ merge_request.assignees.each do |assignee|
+ is_expected.to have_body_text /#{assignee.name}/
+ end
end
end
end
@@ -190,7 +204,7 @@
subject { described_class.unapproved_merge_request_email(recipient.id, merge_request_without_assignee.id, last_unapprover.id) }
before do
- merge_request_without_assignee.approvals.create(user: merge_request_without_assignee.assignee)
+ merge_request_without_assignee.approvals.create(user: merge_request_without_assignee.assignees.first)
end
it 'contains the new status' do
diff --git a/ee/spec/models/merge_request_spec.rb b/ee/spec/models/merge_request_spec.rb
index c01fc5ff5e5ae609ab1e9356ff98705a3f9ce5d9..7cba0536ef6a0269f52a17297c1e1a34dadb719d 100644
--- a/ee/spec/models/merge_request_spec.rb
+++ b/ee/spec/models/merge_request_spec.rb
@@ -24,6 +24,33 @@
let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
end
+ describe '#allows_multiple_assignees?' do
+ it 'does not allow multiple assignees without license' do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+
+ merge_request = build_stubbed(:merge_request)
+
+ expect(merge_request.allows_multiple_assignees?).to be(false)
+ end
+
+ it 'allows multiple assignees when licensed' do
+ stub_licensed_features(multiple_merge_request_assignees: true)
+
+ merge_request = build(:merge_request)
+
+ expect(merge_request.allows_multiple_assignees?).to be(true)
+ end
+
+ it 'does not allows multiple assignees when licensed and feature flag is disabled' do
+ stub_licensed_features(multiple_merge_request_assignees: true)
+ stub_feature_flags(multiple_merge_request_assignees: false)
+
+ merge_request = build(:merge_request)
+
+ expect(merge_request.allows_multiple_assignees?).to be(false)
+ end
+ end
+
describe 'approval_rules' do
context 'when project contains approval_rules' do
let!(:project_rule1) { project.approval_rules.create(name: 'p1') }
diff --git a/ee/spec/requests/api/merge_request_approvals_spec.rb b/ee/spec/requests/api/merge_request_approvals_spec.rb
index 586107d78ee0d9b5e3a5a2972f8c7c9896ecb637..0e180dc40bff5579d90652b3e17ed992f973ed87 100644
--- a/ee/spec/requests/api/merge_request_approvals_spec.rb
+++ b/ee/spec/requests/api/merge_request_approvals_spec.rb
@@ -3,9 +3,10 @@
describe API::MergeRequestApprovals do
set(:user) { create(:user) }
set(:user2) { create(:user) }
+ set(:user3) { create(:user) }
set(:admin) { create(:user, :admin) }
set(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
- set(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: Time.now) }
+ set(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user, user3], source_project: project, target_project: project, title: "Test", created_at: Time.now) }
before do
project.update_attribute(:approvals_before_merge, 2)
@@ -394,7 +395,7 @@ def approve(extra_params = {})
set(:user2) { create(:user) }
set(:admin) { create(:user, :admin) }
set(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
- set(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: Time.now) }
+ set(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: Time.now) }
set(:approver) { create :user }
set(:group) { create :group }
diff --git a/ee/spec/requests/api/merge_requests_spec.rb b/ee/spec/requests/api/merge_requests_spec.rb
index 81d17d5267e98d7c361d22042b77cfa9fdbde421..2f1af475e3aa9f6fb1727c879b0fa2c32b813b6c 100644
--- a/ee/spec/requests/api/merge_requests_spec.rb
+++ b/ee/spec/requests/api/merge_requests_spec.rb
@@ -5,10 +5,11 @@
let(:base_time) { Time.now }
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
- let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user, user2], source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:label) do
create(:label, title: 'label', color: '#FFAABB', project: project)
end
@@ -19,11 +20,49 @@
end
describe 'PUT /projects/:id/merge_requests' do
- context 'when updating existing approval rules' do
- def update_merge_request(params)
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+ def update_merge_request(params)
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
+ end
+
+ context 'multiple assignees' do
+ let(:other_user) { create(:user) }
+ let(:params) do
+ { assignee_ids: [user.id, other_user.id] }
+ end
+
+ context 'when licensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: true)
+ end
+
+ it 'creates merge request with multiple assignees' do
+ update_merge_request(params)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['assignees'].size).to eq(2)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response['assignees'].second['name']).to eq(other_user.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
end
+ context 'when not licensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ it 'creates merge request with a single assignee' do
+ update_merge_request(params)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['assignees'].size).to eq(1)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
+ end
+ end
+
+ context 'when updating existing approval rules' do
let!(:rule) { create(:approval_merge_request_rule, merge_request: merge_request, approvals_required: 1) }
it 'is successful' do
@@ -57,6 +96,40 @@ def create_merge_request(args)
defaults = defaults.merge(args)
post api("/projects/#{project.id}/merge_requests", user), params: defaults
end
+
+ context 'multiple assignees' do
+ context 'when licensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: true)
+ end
+
+ it 'creates merge request with multiple assignees' do
+ create_merge_request(assignee_ids: [user.id, user2.id])
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['assignees'].size).to eq(2)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response['assignees'].second['name']).to eq(user2.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
+ end
+
+ context 'when not licensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ it 'creates merge request with a single assignee' do
+ create_merge_request(assignee_ids: [user.id, user2.id])
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['assignees'].size).to eq(1)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
+ end
+ end
+
context 'between branches projects' do
it "returns merge_request" do
create_merge_request(squash: true)
@@ -142,9 +215,19 @@ def expect_response_contain_exactly(*items)
create(:merge_request_with_approver, :simple, author: user, source_project: project, target_project: project, source_branch: 'other-branch')
end
- let(:another_user) {}
+ context 'filter merge requests by assignee ID' do
+ let!(:merge_request2) do
+ create(:merge_request, :simple, assignees: [user2], source_project: project, target_project: project, source_branch: 'other-branch-2')
+ end
+
+ it 'returns merge requests with given assignee ID' do
+ get api('/merge_requests', user), params: { assignee_id: user2.id }
+
+ expect_response_contain_exactly(merge_request, merge_request2)
+ end
+ end
- context 'request merge requests' do
+ context 'filter merge requests by approver IDs' do
before do
get api('/merge_requests', user), params: { approver_ids: approvers_param, scope: :all }
end
diff --git a/ee/spec/requests/api/v3/github_spec.rb b/ee/spec/requests/api/v3/github_spec.rb
index b677b853766d0247094dbc7f8a939ef8cf12a6b9..8511a14f5df615521862000124759880b94cd784 100644
--- a/ee/spec/requests/api/v3/github_spec.rb
+++ b/ee/spec/requests/api/v3/github_spec.rb
@@ -215,11 +215,12 @@
describe 'repo pulls' do
let(:assignee) { create(:user) }
+ let(:assignee2) { create(:user) }
let!(:merge_request) do
- create(:merge_request, source_project: project, target_project: project, author: user, assignee: assignee)
+ create(:merge_request, source_project: project, target_project: project, author: user, assignees: [assignee])
end
let!(:merge_request_2) do
- create(:merge_request, source_project: project2, target_project: project2, author: user, assignee: assignee)
+ create(:merge_request, source_project: project2, target_project: project2, author: user, assignees: [assignee, assignee2])
end
describe 'GET /-/jira/pulls' do
diff --git a/ee/spec/services/draft_notes/publish_service_spec.rb b/ee/spec/services/draft_notes/publish_service_spec.rb
index 89b64f064c3b702d97630fa850205bafede5f1fd..733c189d945dfab1e33103879d32ceb58a0bfce9 100644
--- a/ee/spec/services/draft_notes/publish_service_spec.rb
+++ b/ee/spec/services/draft_notes/publish_service_spec.rb
@@ -168,10 +168,15 @@ def update_file(file_path, new_content)
context 'with quick actions' do
it 'performs quick actions' do
- create(:draft_note, merge_request: merge_request, author: user, note: "thanks\n/assign #{user.to_reference}")
+ other_user = create(:user)
+ project.add_developer(other_user)
+
+ create(:draft_note, merge_request: merge_request,
+ author: user,
+ note: "thanks\n/assign #{user.to_reference} #{other_user.to_reference}")
expect { publish }.to change { DraftNote.count }.by(-1).and change { Note.count }.by(2)
- expect(merge_request.reload.assignee).to eq(user)
+ expect(merge_request.reload.assignees).to match_array([user, other_user])
expect(merge_request.notes.last).to be_system
end
@@ -179,7 +184,7 @@ def update_file(file_path, new_content)
create(:draft_note, merge_request: merge_request, author: user, note: "/assign #{user.to_reference}")
expect { publish }.to change { DraftNote.count }.by(-1).and change { Note.count }.by(1)
- expect(merge_request.reload.assignee).to eq(user)
+ expect(merge_request.reload.assignees).to eq([user])
expect(merge_request.notes.last).to be_system
end
end
diff --git a/ee/spec/services/ee/notification_service_spec.rb b/ee/spec/services/ee/notification_service_spec.rb
index e51969876b66cbe0affb2409866d713b3db9d5f6..26fec72fb18dea524b33c4a8135432b20e46f3f9 100644
--- a/ee/spec/services/ee/notification_service_spec.rb
+++ b/ee/spec/services/ee/notification_service_spec.rb
@@ -124,8 +124,9 @@ def self.it_should_not_email!
context 'new review' do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ let(:user2) { create(:user) }
let(:reviewer) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignees: [user, user2], author: create(:user)) }
let(:review) { create(:review, merge_request: merge_request, project: project, author: reviewer) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, author: reviewer, review: review) }
@@ -133,7 +134,7 @@ def self.it_should_not_email!
build_team(review.project, merge_request)
project.add_maintainer(merge_request.author)
project.add_maintainer(reviewer)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
create(:diff_note_on_merge_request,
project: project,
@@ -146,7 +147,9 @@ def self.it_should_not_email!
it 'sends emails' do
expect(Notify).not_to receive(:new_review_email).with(review.author.id, review.id)
expect(Notify).not_to receive(:new_review_email).with(@unsubscriber.id, review.id)
- expect(Notify).to receive(:new_review_email).with(merge_request.assignee.id, review.id).and_call_original
+ merge_request.assignee_ids.each do |assignee_id|
+ expect(Notify).to receive(:new_review_email).with(assignee_id, review.id).and_call_original
+ end
expect(Notify).to receive(:new_review_email).with(merge_request.author.id, review.id).and_call_original
expect(Notify).to receive(:new_review_email).with(@u_watcher.id, review.id).and_call_original
expect(Notify).to receive(:new_review_email).with(@u_mentioned.id, review.id).and_call_original
@@ -513,10 +516,11 @@ def add_users_with_subscription(group, issuable)
context 'Merge Requests' do
let(:notification) { NotificationService.new }
let(:assignee) { create(:user) }
+ let(:assignee2) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
+ let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee, assignee2], description: 'cc @participant' }
around do |example|
perform_enqueued_jobs do
@@ -528,7 +532,7 @@ def add_users_with_subscription(group, issuable)
stub_feature_flags(approval_rules: false)
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -537,6 +541,12 @@ def add_users_with_subscription(group, issuable)
end
describe '#new_merge_request' do
+ it 'emails all assignees' do
+ notification.new_merge_request(merge_request, assignee)
+
+ merge_request.assignees.each { |assignee| should_email(assignee) }
+ end
+
context 'when the target project has approvers set' do
let(:project_approvers) { create_list(:user, 3) }
diff --git a/ee/spec/services/quick_actions/interpret_service_spec.rb b/ee/spec/services/quick_actions/interpret_service_spec.rb
index a6b1dc8bb9d8f08d0d1faf6788772cc447625030..e870cbd1776d701db3f84750df83e45d53a2ae6c 100644
--- a/ee/spec/services/quick_actions/interpret_service_spec.rb
+++ b/ee/spec/services/quick_actions/interpret_service_spec.rb
@@ -11,7 +11,8 @@
let(:service) { described_class.new(project, current_user) }
before do
- stub_licensed_features(multiple_issue_assignees: true)
+ stub_licensed_features(multiple_issue_assignees: true,
+ multiple_merge_request_assignees: true)
project.add_developer(current_user)
end
@@ -39,6 +40,42 @@
end
end
end
+
+ context 'Merge Request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'fetches assignees and populates them if content contains /assign' do
+ merge_request.update(assignee_ids: [user.id])
+
+ _, updates = service.execute("/assign @#{user2.username}", merge_request)
+
+ expect(updates[:assignee_ids]).to match_array([user.id, user2.id])
+ end
+
+ context 'assign command with multiple assignees' do
+ it 'fetches assignee and populates assignee_ids if content contains /assign' do
+ merge_request.update(assignee_ids: [user.id])
+
+ _, updates = service.execute("/assign @#{user.username}\n/assign @#{user2.username} @#{user3.username}", issue)
+
+ expect(updates[:assignee_ids]).to match_array([user.id, user2.id, user3.id])
+ end
+
+ context 'unlicensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ it 'does not recognize /assign with multiple user references' do
+ merge_request.update(assignee_ids: [user.id])
+
+ _, updates = service.execute("/assign @#{user2.username} @#{user3.username}", merge_request)
+
+ expect(updates[:assignee_ids]).to match_array([user2.id])
+ end
+ end
+ end
+ end
end
context 'unassign command' do
@@ -69,6 +106,42 @@
expect(updates[:assignee_ids]).to be_empty
end
end
+
+ context 'Merge Request' do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it 'unassigns user if content contains /unassign @user' do
+ merge_request.update(assignee_ids: [user.id, user2.id])
+
+ _, updates = service.execute("/unassign @#{user2.username}", merge_request)
+
+ expect(updates[:assignee_ids]).to match_array([user.id])
+ end
+
+ context 'unassign command with multiple assignees' do
+ it 'unassigns both users if content contains /unassign @user @user1' do
+ merge_request.update(assignee_ids: [user.id, user2.id, user3.id])
+
+ _, updates = service.execute("/unassign @#{user.username} @#{user2.username}", merge_request)
+
+ expect(updates[:assignee_ids]).to match_array([user3.id])
+ end
+
+ context 'unlicensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ it 'does not recognize /unassign @user' do
+ merge_request.update(assignee_ids: [user.id, user2.id, user3.id])
+
+ _, updates = service.execute("/unassign @#{user.username}", merge_request)
+
+ expect(updates[:assignee_ids]).to be_empty
+ end
+ end
+ end
+ end
end
context 'reassign command' do
@@ -77,10 +150,22 @@
context 'Merge Request' do
let(:merge_request) { create(:merge_request, source_project: project) }
- it 'does not recognize /reassign @user' do
- _, updates = service.execute(content, merge_request)
+ context 'unlicensed' do
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ it 'does not recognize /reassign @user' do
+ _, updates = service.execute(content, merge_request)
- expect(updates).to be_empty
+ expect(updates).to be_empty
+ end
+ end
+
+ it 'reassigns user if content contains /reassign @user' do
+ _, updates = service.execute("/reassign @#{current_user.username}", merge_request)
+
+ expect(updates[:assignee_ids]).to match_array([current_user.id])
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 34b4fc4b0deac096de3bb03a21bbce8a3290ce27..0e6ba08d497076ef2538aae86a4120643dc106a1 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -663,7 +663,11 @@ class MergeRequestBasic < ProjectEntity
expose(:user_notes_count) { |merge_request, options| issuable_metadata(merge_request, options, :user_notes_count) }
expose(:upvotes) { |merge_request, options| issuable_metadata(merge_request, options, :upvotes) }
expose(:downvotes) { |merge_request, options| issuable_metadata(merge_request, options, :downvotes) }
- expose :author, :assignee, using: Entities::UserBasic
+ expose :assignee, using: ::API::Entities::UserBasic do |merge_request|
+ merge_request.assignee
+ end
+ expose :author, :assignees, using: Entities::UserBasic
+
expose :source_project_id, :target_project_id
expose :labels do |merge_request|
# Avoids an N+1 query since labels are preloaded
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index a89abdd819f9cd9c59ad2f121ab81693827dc31c..02e62bfb6544c6f8969014ff06739e54adb69b04 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -20,6 +20,7 @@ class MergeRequests < Grape::API
def self.update_params_at_least_one_of
%i[
assignee_id
+ assignee_ids
description
labels
milestone_id
@@ -186,6 +187,7 @@ def handle_merge_request_errors!(errors)
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
+ optional :assignee_ids, type: Array[Integer], desc: 'The array of user IDs to assign issue'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: Array[String], coerce_with: Validations::Types::LabelsList.coerce, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
@@ -233,6 +235,7 @@ def handle_merge_request_errors!(errors)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch)
+ mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute
@@ -336,6 +339,7 @@ def handle_merge_request_errors!(errors)
mr_params = declared_params(include_missing: false)
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present?
+ mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
diff --git a/lib/banzai/reference_parser/merge_request_parser.rb b/lib/banzai/reference_parser/merge_request_parser.rb
index e8147ac591a595f289b4110b66b56a21e1fe58d1..d7bf450465ea80fd076f8f4607d92974be5d7623 100644
--- a/lib/banzai/reference_parser/merge_request_parser.rb
+++ b/lib/banzai/reference_parser/merge_request_parser.rb
@@ -10,7 +10,7 @@ def records_for_nodes(nodes)
nodes,
MergeRequest.includes(
:author,
- :assignee,
+ :assignees,
{
# These associations are primarily used for checking permissions.
# Eager loading these ensures we don't end up running dozens of
diff --git a/lib/gitlab/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
index 0803df6563257fb3fd697ca35d69862f53e1e2f6..b8da6731081a81f44c29a048bc08e0be35b15cbb 100644
--- a/lib/gitlab/hook_data/issuable_builder.rb
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -20,11 +20,7 @@ def build(user: nil, changes: {})
repository: issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)
}
- if issuable.is_a?(Issue)
- hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
- else
- hook_data[:assignee] = issuable.assignee.hook_attrs if issuable.assignee
- end
+ hook_data[:assignees] = issuable.assignees.map(&:hook_attrs) if issuable.assignees.any?
hook_data
end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index d77b1d046446bca634d886095e88f9ea75f19db3..a8e993e087e700e9175ba33e0a0f703c4cf9f579 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -34,7 +34,6 @@ def self.safe_hook_attributes
end
SAFE_HOOK_RELATIONS = %i[
- assignee
labels
total_time_spent
].freeze
@@ -51,7 +50,9 @@ def build
work_in_progress: merge_request.work_in_progress?,
total_time_spent: merge_request.total_time_spent,
human_total_time_spent: merge_request.human_total_time_spent,
- human_time_estimate: merge_request.human_time_estimate
+ human_time_estimate: merge_request.human_time_estimate,
+ assignee_ids: merge_request.assignee_ids,
+ assignee_id: merge_request.assignee_ids.first # This key is deprecated
}
merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 70da0158580f60ca21e9cae189d7f651ada8fe52..b0278643c3baccddcc996bcc232a369da867254e 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1338,9 +1338,6 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
-msgid "Assigned to :name"
-msgstr ""
-
msgid "Assigned to me"
msgstr ""
@@ -7354,9 +7351,6 @@ msgstr ""
msgid "No activities found"
msgstr ""
-msgid "No assignee"
-msgstr ""
-
msgid "No branches found"
msgstr ""
@@ -7468,9 +7462,6 @@ msgstr ""
msgid "None"
msgstr ""
-msgid "Not allowed to merge"
-msgstr ""
-
msgid "Not available"
msgstr ""
@@ -9692,9 +9683,6 @@ msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
-msgid "Select assignee"
-msgstr ""
-
msgid "Select branch/tag"
msgstr ""
@@ -12846,9 +12834,6 @@ msgstr ""
msgid "among other things"
msgstr ""
-msgid "assign yourself"
-msgstr ""
-
msgid "at"
msgstr ""
diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb
index 20d9c3363672b43be42c2d2c3db034672388ba80..56fbf59b9bcc7ba9266c13db43387cf0a07b01dd 100644
--- a/qa/qa/page/merge_request/new.rb
+++ b/qa/qa/page/merge_request/new.rb
@@ -26,7 +26,7 @@ class New < Page::Base
element :issuable_label
end
- view 'app/views/shared/issuable/form/_metadata_merge_request_assignee.html.haml' do
+ view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
element :assign_to_me_link
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 017162519d835cd4aa4b226135cf1038cf16f3d9..a125e4705227f0b14282ccf52904007e422654bc 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -238,11 +238,11 @@ def update_merge_request(mr_params, additional_params = {})
assignee = create(:user)
project.add_developer(assignee)
- update_merge_request({ assignee_id: assignee.id }, format: :json)
+ update_merge_request({ assignee_ids: [assignee.id] }, format: :json)
+
body = JSON.parse(response.body)
- expect(body['assignee'].keys)
- .to match_array(%w(name username avatar_url id state web_url))
+ expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url)))
end
end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index fbc2e5cc3d3b2f3cc90a825c46dad7c354121ae2..50b71368e1349589d72534ad43a08d5066691792 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -8,7 +8,7 @@
before do
issue.assignees = [user]
- merge_request.update(assignee: user)
+ merge_request.update(assignees: [user])
sign_in(user)
end
@@ -33,7 +33,7 @@
expect_counters('merge_requests', '1')
- merge_request.update(assignee: nil)
+ merge_request.update(assignees: [])
user.invalidate_cache_counts
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 322cdbd50afa38c1c76b0b7ae1c2ae5201a8b81d..0c1e1d5910b2f52305cc61784bddadf9fb0b6f9f 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -50,14 +50,14 @@
let!(:assigned_merge_request) do
create(:merge_request,
- assignee: current_user,
+ assignees: [current_user],
source_project: project,
author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
- source_branch: 'markdown', assignee: current_user,
+ source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
author: create(:user))
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 54a8016c1575f295c287141330ac7c1b247fa1c4..e1bc4eca61925300cab4efb21a70825cef43d4f2 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -38,7 +38,7 @@
context 'when merge request assignee to user' do
before do
- issuable.update!(assignee: user)
+ issuable.update!(assignees: [user])
visit path
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index d0fdd469260bb2a5fe71a70ee015e96c4d63b1a7..597af566f9c400b8b390e4aed6ac3007aa3a71c9 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -32,8 +32,8 @@
# the original method, resulting in infinite recursion when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
- original_issue_dropdown_options = EE::FormHelper.instance_method(:issue_assignees_dropdown_options)
- allow_any_instance_of(EE::FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
+ original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options)
+ allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index 061a4c4f63399ca6e01832f622ac9d16ef9e119e..df4e4ed86ceba2a83ac0baaffea076071d7379f4 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -108,15 +108,15 @@
fill_in("Title", with: title)
end
- click_button("Assignee")
-
expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
+ find('.js-assignee-search').click
page.within(".dropdown-menu-user") do
expect(page).to have_content("Unassigned")
.and have_content(user.name)
.and have_content(project.users.first.name)
end
+ find('.js-assignee-search').click
click_button("Submit merge request")
diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb
index c169a68cd1cdfc8e2611d5776d56749ee164b976..c9dedab048abcb7793d26d2be8704d68a4f6e984 100644
--- a/spec/features/merge_request/user_creates_mr_spec.rb
+++ b/spec/features/merge_request/user_creates_mr_spec.rb
@@ -1,11 +1,18 @@
require 'rails_helper'
describe 'Merge request > User creates MR' do
- it_behaves_like 'a creatable merge request'
+ include ProjectForksHelper
- context 'from a forked project' do
- include ProjectForksHelper
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+ context 'non-fork merge request' do
+ include_context 'merge request create context'
+ it_behaves_like 'a creatable merge request'
+ end
+
+ context 'from a forked project' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:source_project) do
@@ -15,6 +22,7 @@
end
context 'to canonical project' do
+ include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
@@ -25,6 +33,7 @@
namespace: user.namespace)
end
+ include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
end
diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb
index 3152707136cbc1fc392455aacb77d7f50de1ccb9..25979513ead8378bb7588488d6d8759b1331ab51 100644
--- a/spec/features/merge_request/user_edits_mr_spec.rb
+++ b/spec/features/merge_request/user_edits_mr_spec.rb
@@ -1,13 +1,21 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Merge request > User edits MR' do
include ProjectForksHelper
- it_behaves_like 'an editable merge request'
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ context 'non-fork merge request' do
+ include_context 'merge request edit context'
+ it_behaves_like 'an editable merge request'
+ end
context 'for a forked project' do
- it_behaves_like 'an editable merge request' do
- let(:source_project) { fork_project(target_project, nil, repository: true) }
- end
+ let(:source_project) { fork_project(target_project, nil, repository: true) }
+
+ include_context 'merge request edit context'
+ it_behaves_like 'an editable merge request'
end
end
diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
index d6c770c93f12cc38e93a9639832df70646f2ca57..0cbf1bcae300ab86fe9c0aa50a735a9fcf675aa1 100644
--- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
@@ -7,7 +7,7 @@
let(:user) { project.creator }
before do
- create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
+ create(:merge_request, assignees: [user], title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2')
sign_in(user)
diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
index 1615899a047461da8d63ccfc68ea7bd0fcb42fed..4627931f26a44683b7deb415f1d54f7efb985607 100644
--- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
@@ -10,7 +10,7 @@
before do
sign_in(user)
- mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone)
+ mr = create(:merge_request, title: 'Bugfix2', author: user, assignees: [user], source_project: project, target_project: project, milestone: milestone)
mr.labels << wontfix
visit project_merge_requests_path(project)
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index c691011b9ca078c3d9502c79ce6d1c2ec85aa855..bd91fae1453b0d55dbdf2d73327aeec2382edef4 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -12,7 +12,7 @@
title: 'fix',
source_project: project,
source_branch: 'fix',
- assignee: user,
+ assignees: [user],
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago,
updated_at: 1.minute.ago)
@@ -20,7 +20,7 @@
title: 'markdown',
source_project: project,
source_branch: 'markdown',
- assignee: user,
+ assignees: [user],
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index e535c7e5811394ce3602cc918eee9a0ab926a4e4..c2dd105324d7b9fb4eff85b98ab13791c507dcea 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -54,8 +54,7 @@
describe 'remove assignee' do
before do
- merge_request.assignee = user
- merge_request.save
+ merge_request.assignees = [user]
visit project_merge_requests_path(project)
end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 444de26733f591f0b52c45e25d5470204cbbe7f7..1cc47cd6bd1216dc233ec510f1cab46d250f2028 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -36,7 +36,7 @@
end
context 'when clicking merge requests' do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me')
@@ -100,7 +100,7 @@
end
context 'when clicking merge requests' do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.dropdown-menu').click_link('Merge requests assigned to me')
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index fe53fabe54ced24cdc53e0f3126fc86cd565cea9..6e6dc3343545af5e4bcc5968d43f66074a3dea67 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -13,60 +13,32 @@
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
- context 'filtering by assignee ID' do
- let(:params) { { assignee_id: user.id } }
+ context 'assignee filtering' do
+ let(:issuables) { issues }
- it 'returns issues assigned to that user' do
- expect(issues).to contain_exactly(issue1, issue2)
- end
- end
-
- context 'filtering by assignee usernames' do
- set(:user3) { create(:user) }
- let(:params) { { assignee_username: [user2.username, user3.username] } }
-
- before do
- project2.add_developer(user3)
-
- issue3.assignees = [user2, user3]
- end
-
- it 'returns issues assigned to those users' do
- expect(issues).to contain_exactly(issue3)
- end
- end
-
- context 'filtering by no assignee' do
- let(:params) { { assignee_id: 'None' } }
-
- it 'returns issues not assigned to any assignee' do
- expect(issues).to contain_exactly(issue4)
- end
-
- it 'returns issues not assigned to any assignee' do
- params[:assignee_id] = 0
-
- expect(issues).to contain_exactly(issue4)
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [issue1, issue2] }
end
- it 'returns issues not assigned to any assignee' do
- params[:assignee_id] = 'none'
+ it_behaves_like 'assignee username filter' do
+ before do
+ project2.add_developer(user3)
+ issue3.assignees = [user2, user3]
+ end
- expect(issues).to contain_exactly(issue4)
+ set(:user3) { create(:user) }
+ let(:params) { { assignee_username: [user2.username, user3.username] } }
+ let(:expected_issuables) { [issue3] }
end
- end
- context 'filtering by any assignee' do
- let(:params) { { assignee_id: 'Any' } }
-
- it 'returns issues assigned to any assignee' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ it_behaves_like 'no assignee filter' do
+ set(:user3) { create(:user) }
+ let(:expected_issuables) { [issue4] }
end
- it 'returns issues assigned to any assignee' do
- params[:assignee_id] = 'any'
-
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [issue1, issue2, issue3] }
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index f508b9bdb6f068dc898e3a11e9166abdf6560134..ef00212563532f7548075bb09f2a312a790a66e8 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -136,21 +136,50 @@
end
end
- context 'filtering by group milestone' do
- let(:group_milestone) { create(:milestone, group: group) }
+ context 'assignee filtering' do
+ let(:issuables) { described_class.new(user, params).execute }
- before do
- project2.update(namespace: group)
- merge_request2.update(milestone: group_milestone)
- merge_request3.update(milestone: group_milestone)
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [merge_request1, merge_request2] }
end
- it 'returns merge requests assigned to that group milestone' do
- params = { milestone_title: group_milestone.title }
+ it_behaves_like 'assignee username filter' do
+ before do
+ project2.add_developer(user3)
+ merge_request3.assignees = [user2, user3]
+ end
- merge_requests = described_class.new(user, params).execute
+ set(:user3) { create(:user) }
+ let(:params) { { assignee_username: [user2.username, user3.username] } }
+ let(:expected_issuables) { [merge_request3] }
+ end
+
+ it_behaves_like 'no assignee filter' do
+ set(:user3) { create(:user) }
+ let(:expected_issuables) { [merge_request4, merge_request5] }
+ end
+
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
+ end
+
+ context 'filtering by group milestone' do
+ let(:group_milestone) { create(:milestone, group: group) }
+
+ before do
+ project2.update(namespace: group)
+ merge_request2.update(milestone: group_milestone)
+ merge_request3.update(milestone: group_milestone)
+ end
+
+ it 'returns merge requests assigned to that group milestone' do
+ params = { milestone_title: group_milestone.title }
- expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
+ end
end
end
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 3006b482d41bdd107ee6015849cf2e0a2917568a..88a600398b14627d085195f708bbce67991b8905 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -6,14 +6,14 @@
"source_branch_exists": { "type": "boolean" },
"merge_error": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
- "assignee_id": { "type": ["integer", "null"] },
"allow_collaboration": { "type": "boolean"},
"allow_maintainer_to_push": { "type": "boolean"},
- "assignee": {
- "oneOf": [
- { "type": "null" },
- { "$ref": "user.json" }
- ]
+ "assignees": {
+ "type": ["array"],
+ "items": {
+ "type": "object",
+ "$ref": "../public_api/v4/user/basic.json"
+ }
},
"milestone": {
"type": [ "object", "null" ]
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
index 918f2c4b47db37140444e504e4406b049acb4fb4..a423bf70b6943621cb4550b998b096aea45fb219 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_request.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -64,6 +64,11 @@
},
"additionalProperties": false
},
+ "assignees": {
+ "items": {
+ "$ref": "./merge_request.json"
+ }
+ },
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index 57b16b12cb04b654ccbb6ceea2bede8d5836865d..47fee5d2b214df30507d64e5f8c92cc9e5c10aef 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -132,9 +132,94 @@ describe('Assignee component', () => {
-1,
);
});
+
+ it('has correct "cannot merge" tooltip when user cannot merge', () => {
+ const user = Object.assign({}, UsersMock.user, { can_merge: false });
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users: [user],
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
+ });
});
describe('Two or more assignees/users', () => {
+ it('has correct "cannot merge" tooltip when one user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = true;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when no user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when more than one user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
+ });
+
+ it('has no "cannot merge" tooltip when every user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual(null);
+ });
+
it('displays two assignee icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
component = new AssigneeComponent({
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
index 26529c4759d9bb654ba0e68d74fbebc2ebdc238f..569d5dcc75721b1a9f57f16532037b2362c9c4d7 100644
--- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -97,13 +97,13 @@
end
context 'merge_request is assigned' do
- let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:data) { described_class.new(merge_request).build(user: user) }
it 'returns correct hook data' do
expect(data[:object_attributes]['assignee_id']).to eq(user.id)
- expect(data[:assignee]).to eq(user.hook_attrs)
- expect(data).not_to have_key(:assignees)
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignee)
end
end
end
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index 9ce697adbbae77a289ced4817a5b1a2dd63ad91c..39f80f92fa6b63ed9ff7455591cc15298918960d 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -10,6 +10,7 @@
it 'includes safe attribute' do
%w[
assignee_id
+ assignee_ids
author_id
created_at
description
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e760c7f7358b921991ef3ffa8de61440882ce081..6018f64fad62221d3d2b8686a7114f6fd2126287 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -102,6 +102,7 @@ merge_requests:
- merge_request_pipelines
- merge_request_assignees
- suggestions
+- assignees
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -333,6 +334,9 @@ push_event_payload:
issue_assignees:
- issue
- assignee
+merge_request_assignees:
+- merge_request
+- assignee
lfs_file_locks:
- user
project_badges:
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index b586f6e0be3f20d299bd8892aab3f14f40451708..add442488d8533dee4d0dce8c5bbe2ae7c549bc5 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -669,3 +669,7 @@ Suggestion:
- outdated
- lines_above
- lines_below
+MergeRequestAssignee:
+- id
+- user_id
+- merge_request_id
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 6ec861632336e5845a5d56c4e5214044a2d6e8fd..916f3876a8e916e1e0d06b6560e73b43203cd94f 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -19,7 +19,7 @@
let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
@@ -39,7 +39,7 @@
end
context 'merge requests' do
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 5fa1369c00ad2fc5c2148bffd2a5b22353901cdc..fee1d701e3ad6ed431f54fa5d540547d2c3e0ac3 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -19,7 +19,7 @@
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee],
description: 'Awesome description')
end
@@ -275,7 +275,7 @@
context 'for merge requests' do
describe 'that are new' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
@@ -300,7 +300,7 @@
end
context 'when sent with a reason' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id, NotificationReason::ASSIGNED) }
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
@@ -324,7 +324,7 @@
describe 'that are reassigned' do
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -351,7 +351,7 @@
end
context 'when sent with a reason' do
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) }
it_behaves_like 'appearance header and footer enabled'
it_behaves_like 'appearance header and footer not enabled'
@@ -364,11 +364,11 @@
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED)
is_expected.to have_body_text(text)
- new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED)
+ new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED)
expect(new_subject).to have_body_text(text)
- new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil)
+ new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil)
expect(new_subject).to have_body_text(text)
end
@@ -376,7 +376,7 @@
end
describe 'that are new with a description' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
@@ -476,7 +476,7 @@
source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee],
description: 'Awesome description')
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 83b0f172f03e3965e170bf7a53305cdee7cc6347..f3e78630c1b7093114d96f447206a330a4862509 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -684,12 +684,12 @@ def create_build(name, status)
source_branch: 'feature',
target_project: project,
target_branch: 'master',
- assignee: assignee,
+ assignees: assignees,
milestone: milestone,
labels: labels)
end
- let(:assignee) { create(:user) }
+ let(:assignees) { create_list(:user, 2) }
let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2) }
@@ -710,7 +710,7 @@ def create_build(name, status)
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s,
'CI_MERGE_REQUEST_TITLE' => merge_request.title,
- 'CI_MERGE_REQUEST_ASSIGNEES' => assignee.username,
+ 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(','))
end
@@ -730,7 +730,7 @@ def create_build(name, status)
end
context 'without assignee' do
- let(:assignee) { nil }
+ let(:assignees) { [] }
it 'does not expose assignee variable' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
diff --git a/spec/models/concerns/deprecated_assignee_spec.rb b/spec/models/concerns/deprecated_assignee_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e394de0aa34f6cd2a83701c4a3d95f41227404ad
--- /dev/null
+++ b/spec/models/concerns/deprecated_assignee_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeprecatedAssignee do
+ let(:user) { create(:user) }
+
+ describe '#assignee_id=' do
+ it 'creates the merge_request_assignees relation' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ merge_request.reload
+
+ expect(merge_request.merge_request_assignees.count).to eq(1)
+ end
+
+ it 'nullifies the assignee_id column' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ merge_request.reload
+
+ expect(merge_request.read_attribute(:assignee_id)).to be_nil
+ end
+
+ context 'when relation already exists' do
+ it 'overwrites existing assignees' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.merge_request_assignees.create!(user_id: user.id)
+ merge_request.merge_request_assignees.create!(user_id: other_user.id)
+
+ expect { merge_request.update!(assignee_id: other_user.id) }
+ .to change { merge_request.reload.merge_request_assignees.count }
+ .from(2).to(1)
+ end
+ end
+ end
+
+ describe '#assignee=' do
+ it 'creates the merge_request_assignees relation' do
+ merge_request = create(:merge_request, assignee: user)
+
+ merge_request.reload
+
+ expect(merge_request.merge_request_assignees.count).to eq(1)
+ end
+
+ it 'nullifies the assignee_id column' do
+ merge_request = create(:merge_request, assignee: user)
+
+ merge_request.reload
+
+ expect(merge_request.read_attribute(:assignee_id)).to be_nil
+ end
+
+ context 'when relation already exists' do
+ it 'overwrites existing assignees' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignee: nil)
+ merge_request.merge_request_assignees.create!(user_id: user.id)
+ merge_request.merge_request_assignees.create!(user_id: other_user.id)
+
+ expect { merge_request.update!(assignee: other_user) }
+ .to change { merge_request.reload.merge_request_assignees.count }
+ .from(2).to(1)
+ end
+ end
+ end
+
+ describe '#assignee_id' do
+ it 'returns the first assignee ID' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignees: [user, other_user])
+
+ merge_request.reload
+
+ expect(merge_request.assignee_id).to eq(merge_request.assignee_ids.first)
+ end
+ end
+
+ describe '#assignees' do
+ context 'when assignee_id exists and there is no relation' do
+ it 'creates the relation' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignees }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
+ end
+
+ it 'nullifies the assignee_id' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignees }
+ .to change { merge_request.read_attribute(:assignee_id) }
+ .from(user.id).to(nil)
+ end
+ end
+
+ context 'when DB is read-only' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'returns a users relation' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
+ expect(merge_request.assignees).to eq([user])
+ end
+
+ it 'returns an empty relation if no assignee_id is set' do
+ merge_request = create(:merge_request, assignee_id: nil)
+
+ expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
+ expect(merge_request.assignees).to eq([])
+ end
+ end
+ end
+
+ describe '#assignee_ids' do
+ context 'when assignee_id exists and there is no relation' do
+ it 'creates the relation' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignee_ids }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
+ end
+
+ it 'nullifies the assignee_id' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignee_ids }
+ .to change { merge_request.read_attribute(:assignee_id) }
+ .from(user.id).to(nil)
+ end
+ end
+
+ context 'when DB is read-only' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'returns a list of user IDs' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ expect(merge_request.assignee_ids).to be_a(Array)
+ expect(merge_request.assignee_ids).to eq([user.id])
+ end
+
+ it 'returns an empty relation if no assignee_id is set' do
+ merge_request = create(:merge_request, assignee_id: nil)
+
+ expect(merge_request.assignee_ids).to be_a(Array)
+ expect(merge_request.assignee_ids).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 27ed298ae08baf3f6241624ea58ff78cf3656945..64f02978d79576bf58e9a4ad273756500660d0b5 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -502,8 +502,8 @@ def build_issuable(milestone_id)
let(:user2) { create(:user) }
before do
- merge_request.update(assignee: user)
- merge_request.update(assignee: user2)
+ merge_request.update(assignees: [user])
+ merge_request.update(assignees: [user, user2])
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(merge_request).and_return(builder)
end
@@ -512,8 +512,7 @@ def build_issuable(milestone_id)
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'assignee_id' => [user.id, user2.id],
- 'assignee' => [user.hook_attrs, user2.hook_attrs]
+ 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
))
merge_request.to_hook_data(user, old_associations: { assignees: [user] })
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index d192fe70506d74b7d1d1aea8f0a2fa85f3a9ebf4..e91b5c4c86ff89ef9594c4936d2196358098bcb7 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -263,7 +263,7 @@
context 'merge request diff note event' do
let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) }
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6f34ef9c1bcf89e66fa6e1631a5bd0b34e66d18c..f61857ea5ff062ef1f26e8ddff536c7e96d9d42b 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -13,7 +13,7 @@
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to belong_to(:assignee) }
+ it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) }
context 'for forks' do
@@ -181,31 +181,6 @@
expect(MergeRequest::Metrics.count).to eq(1)
end
end
-
- describe '#refresh_merge_request_assignees' do
- set(:user) { create(:user) }
-
- it 'creates merge request assignees relation upon MR creation' do
- merge_request = create(:merge_request, assignee: nil)
-
- expect(merge_request.merge_request_assignees).to be_empty
-
- expect { merge_request.update!(assignee: user) }
- .to change { merge_request.reload.merge_request_assignees.count }
- .from(0).to(1)
- end
-
- it 'updates merge request assignees relation upon MR assignee change' do
- another_user = create(:user)
- merge_request = create(:merge_request, assignee: user)
-
- expect { merge_request.update!(assignee: another_user) }
- .to change { merge_request.reload.merge_request_assignees.first.assignee }
- .from(user).to(another_user)
-
- expect(merge_request.merge_request_assignees.count).to eq(1)
- end
- end
end
describe 'respond to' do
@@ -337,34 +312,18 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
- allow(subject).to receive(:assignee).and_return(nil)
+ allow(subject).to receive(:assignees).and_return([])
expect(subject.card_attributes)
- .to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ .to eq({ 'Author' => 'Robert', 'Assignee' => "" })
end
- it 'includes the assignee name' do
+ it 'includes the assignees name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
- allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
+ allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
expect(subject.card_attributes)
- .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
- end
- end
-
- describe '#assignee_ids' do
- it 'returns an array of the assigned user id' do
- subject.assignee_id = 123
-
- expect(subject.assignee_ids).to eq([123])
- end
- end
-
- describe '#assignee_ids=' do
- it 'sets assignee_id to the last id in the array' do
- subject.assignee_ids = [123, 456]
-
- expect(subject.assignee_id).to eq(456)
+ .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
end
end
@@ -372,7 +331,7 @@ def create_merge_request_with_diffs(source_branch, diffs: 2)
let(:user) { create(:user) }
it 'returns true for a user that is assigned to a merge request' do
- subject.assignee = user
+ subject.assignees = [user]
expect(subject.assignee_or_author?(user)).to eq(true)
end
@@ -1949,15 +1908,14 @@ def set_compare(merge_request)
it 'updates when assignees change' do
user1 = create(:user)
user2 = create(:user)
- mr = create(:merge_request, assignee: user1)
+ mr = create(:merge_request, assignees: [user1])
mr.project.add_developer(user1)
mr.project.add_developer(user2)
expect(user1.assigned_open_merge_requests_count).to eq(1)
expect(user2.assigned_open_merge_requests_count).to eq(0)
- mr.assignee = user2
- mr.save
+ mr.assignees = [user2]
expect(user1.assigned_open_merge_requests_count).to eq(0)
expect(user2.assigned_open_merge_requests_count).to eq(1)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a45a2737b130ea04a7d1cb9bde3fe9a6cf235045..d1338e34bb81bdc7955bd4db92a13886d147d9fc 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2816,9 +2816,9 @@ def add_user(access)
project = create(:project, :public)
archived_project = create(:project, :public, :archived)
- create(:merge_request, source_project: project, author: user, assignee: user)
- create(:merge_request, :closed, source_project: project, author: user, assignee: user)
- create(:merge_request, source_project: archived_project, author: user, assignee: user)
+ create(:merge_request, source_project: project, author: user, assignees: [user])
+ create(:merge_request, :closed, source_project: project, author: user, assignees: [user])
+ create(:merge_request, source_project: archived_project, author: user, assignees: [user])
expect(user.assigned_open_merge_requests_count(force: true)).to eq 1
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 0ac23505de7d931d2b382369e1a75b64e43c1b77..065b16c6221e622c85d6fb1aa97ce8994a5bc1e5 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -270,8 +270,8 @@
end
context 'when exists some events' do
- let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
+ let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
before do
create_event(merge_request1)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 628841d16ebe62fd545bad774cd29fe553caee10..52c214ad08cef283aabce26939c874e9b2167753 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -5,14 +5,15 @@
let(:base_time) { Time.now }
set(:user) { create(:user) }
+ set(:user2) { create(:user) }
set(:admin) { create(:user, :admin) }
let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
- let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
- let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) }
@@ -20,6 +21,9 @@
before do
project.add_reporter(user)
+ project.add_reporter(user2)
+
+ stub_licensed_features(multiple_merge_request_assignees: false)
end
shared_context 'with labels' do
@@ -45,9 +49,9 @@
get api(endpoint_path, user)
end
- create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+ create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time)
- merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
+ merge_request = create(:merge_request, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time)
merge_request.metrics.update!(merged_by: user,
latest_closed_by: user,
@@ -333,7 +337,7 @@
state: 'closed',
milestone: milestone1,
author: user,
- assignee: user,
+ assignees: [user],
source_project: project,
target_project: project,
title: "Test",
@@ -451,7 +455,7 @@
context 'when authenticated' do
let!(:project2) { create(:project, :public, namespace: user.namespace) }
- let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) }
+ let!(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) }
let(:user2) { create(:user) }
it 'returns an array of all merge requests except unauthorized ones' do
@@ -494,7 +498,7 @@
end
it 'returns an array of merge requests created by current user if no scope is given' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2)
@@ -502,7 +506,7 @@
end
it 'returns an array of merge requests authored by the given user' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
@@ -510,7 +514,7 @@
end
it 'returns an array of merge requests assigned to the given user' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all }
@@ -535,7 +539,7 @@
end
it 'returns an array of merge requests assigned to me' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'assigned_to_me' }
@@ -543,7 +547,7 @@
end
it 'returns an array of merge requests assigned to me (kebab-case)' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'assigned-to-me' }
@@ -551,7 +555,7 @@
end
it 'returns an array of merge requests created by me' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'created_by_me' }
@@ -559,7 +563,7 @@
end
it 'returns an array of merge requests created by me (kebab-case)' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'created-by-me' }
@@ -567,7 +571,7 @@
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star')
get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' }
@@ -700,7 +704,7 @@
get api("/projects/#{project.id}/merge_requests", user)
end.count
- create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, created_at: base_time)
+ create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time)
expect do
get api("/projects/#{project.id}/merge_requests", user)
@@ -730,7 +734,7 @@
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
it 'matches json schema' do
- merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
+ merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time)
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_gitlab_http_status(200)
@@ -851,7 +855,7 @@
end
context 'Work in Progress' do
- let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
it "returns merge request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
@@ -867,7 +871,7 @@
merge_request_overflow = create(:merge_request, :simple,
author: user,
- assignee: user,
+ assignees: [user],
source_project: project,
source_branch: 'expand-collapse-files',
target_project: project,
@@ -1005,6 +1009,71 @@
end
describe 'POST /projects/:id/merge_requests' do
+ context 'support for deprecated assignee_id' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ assignee_id: user2.id
+ }
+ end
+
+ it 'creates a new merge request' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new merge request when assignee_id is empty' do
+ params[:assignee_id] = ''
+
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignee']).to be_nil
+ end
+
+ it 'filters assignee_id of unauthorized user' do
+ private_project = create(:project, :private, :repository)
+ another_user = create(:user)
+ private_project.add_maintainer(user)
+ params[:assignee_id] = another_user.id
+
+ post api("/projects/#{private_project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['assignee']).to be_nil
+ end
+ end
+
+ context 'single assignee restrictions' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ assignee_ids: [user.id, user2.id]
+ }
+ end
+
+ it 'creates a new project merge request with no more than one assignee' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignees'].count).to eq(1)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
+ end
+
context 'between branches projects' do
context 'different labels' do
let(:params) do
@@ -1595,6 +1664,19 @@
expect(json_response['force_remove_source_branch']).to be_truthy
end
+ it 'filters assignee_id of unauthorized user' do
+ private_project = create(:project, :private, :repository)
+ mr = create(:merge_request, source_project: private_project, target_project: private_project)
+ another_user = create(:user)
+ private_project.add_maintainer(user)
+ params = { assignee_id: another_user.id }
+
+ put api("/projects/#{private_project.id}/merge_requests/#{mr.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['assignee']).to be_nil
+ end
+
context 'when updating labels' do
it 'allows special label names' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
@@ -1749,7 +1831,7 @@
issue = create(:issue, project: jira_project)
description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}"
merge_request = create(:merge_request,
- :simple, author: user, assignee: user, source_project: jira_project, description: description)
+ :simple, author: user, assignees: [user], source_project: jira_project, description: description)
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index ca366cdf1dfa2bbedff8884322c0b69772f093c3..363b7266940147e07c9b032ee068bf685491f3f9 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -76,14 +76,14 @@ def bulk_update(issuables, extra_params = {})
end
describe 'updating merge request assignee' do
- let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) }
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
new_assignee = create(:user)
project.add_developer(new_assignee)
- result = bulk_update(merge_request, assignee_id: new_assignee.id)
+ result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id])
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
@@ -93,22 +93,22 @@ def bulk_update(issuables, extra_params = {})
assignee = create(:user)
project.add_developer(assignee)
- expect { bulk_update(merge_request, assignee_id: assignee.id) }
- .to change { merge_request.reload.assignee }.from(user).to(assignee)
+ expect { bulk_update(merge_request, assignee_ids: [assignee.id]) }
+ .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id])
end
end
context "when the new assignee ID is #{IssuableFinder::NONE}" do
it 'unassigns the issues' do
- expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) }
- .to change { merge_request.reload.assignee }.to(nil)
+ expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) }
+ .to change { merge_request.reload.assignee_ids }.to([])
end
end
context 'when the new assignee ID is not present' do
it 'does not unassign' do
- expect { bulk_update(merge_request, assignee_id: nil) }
- .not_to change { merge_request.reload.assignee }
+ expect { bulk_update(merge_request, assignee_ids: []) }
+ .not_to change { merge_request.reload.assignee_ids }
end
end
end
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index 8ccbba7fa58195bfc8e7036259c4cfb114db91c1..15d1bb73ca3d1c2016062fc6b3fa57b46621e6ec 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -34,7 +34,7 @@
end
context 'when issuable is a merge request' do
- let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignees: [user]) }
it 'destroys the merge request' do
expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1)
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index ce703e11a68ccf0c5fd9aaa9b7c7e5fa727f16bc..91bf4dccd77ce7e99841127be5a8b3dcd36bcc2f 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -45,7 +45,7 @@
it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures do
create(:issue, project: group_project, assignees: [member_user])
- create(:merge_request, source_project: group_project, assignee: member_user)
+ create(:merge_request, source_project: group_project, assignees: [member_user])
create(:todo, :pending, project: group_project, user: member_user)
create(:todo, :done, project: group_project, user: member_user)
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 433ffbd97f015845ddb286fac6908f095e27ec56..706bcea819955ed8549612c0a970c9993721d666 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -4,7 +4,7 @@
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 393299cce00ba914833505a90bdc38de62bd35c8..20bf1cbb8b6780ca8e38de6b982da3b805d64376 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -118,7 +118,7 @@
result = service.execute
- expect(result[:merge_request].assignee).to eq(user)
+ expect(result[:merge_request].assignees).to eq([user])
end
context 'when ref branch is set' do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index dc5d1cf2f0447e78c9a62a23834ff8704a362f4e..30271e04c8e13ced1469b2ec27195334768a586f 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -32,7 +32,7 @@
expect(merge_request).to be_valid
expect(merge_request.work_in_progress?).to be(false)
expect(merge_request.title).to eq('Awesome merge_request')
- expect(merge_request.assignee).to be_nil
+ expect(merge_request.assignees).to be_empty
expect(merge_request.merge_params['force_remove_source_branch']).to eq('1')
end
@@ -73,7 +73,7 @@
description: "well this is not done yet\n/wip",
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
@@ -89,7 +89,7 @@
description: "well this is not done yet\n/wip",
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
@@ -106,11 +106,11 @@
description: 'please fix',
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
- it { expect(merge_request.assignee).to eq assignee }
+ it { expect(merge_request.assignees).to eq([assignee]) }
it 'creates a todo for new assignee' do
attributes = {
@@ -301,7 +301,7 @@
let(:opts) do
{
- assignee_id: create(:user).id,
+ assignee_ids: create(:user).id,
milestone_id: 1,
title: 'Title',
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
@@ -317,7 +317,7 @@
it 'assigns and sets milestone to issuable from command' do
expect(merge_request).to be_persisted
- expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.assignees).to eq([assignee])
expect(merge_request.milestone).to eq(milestone)
end
end
@@ -332,28 +332,28 @@
end
it 'removes assignee_id when user id is invalid' do
- opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'removes assignee_id when user id is 0' do
- opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'saves assignee when user id is valid' do
project.add_maintainer(assignee)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.assignees).to eq([assignee])
end
context 'when assignee is set' do
@@ -361,7 +361,7 @@
{
title: 'Title',
description: 'Description',
- assignee_id: assignee.id,
+ assignee_ids: [assignee.id],
source_branch: 'feature',
target_branch: 'master'
}
@@ -387,7 +387,7 @@
levels.each do |level|
it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
project.update(visibility_level: level)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
merge_request = described_class.new(project, user, opts).execute
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index 1430e12a07ec0c9e42bf4c8b36af02e65a7518f2..a87d8b8752cd6ea6ba5bea90bc098b0238fbf79f 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -7,7 +7,7 @@
create(:merge_request,
source_branch: 'flatten-dir',
target_branch: 'improve/awesome',
- assignee: user2,
+ assignees: [user2],
author: create(:user))
end
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 887ec17171ec35962243fc34ba0b6f0fe4f5c817..b0b3273e3dc8bb356794825050811e9706a0d5b3 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -3,7 +3,7 @@
describe MergeRequests::MergeService do
set(:user) { create(:user) }
set(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) }
+ let(:merge_request) { create(:merge_request, :simple, author: user2, assignees: [user2]) }
let(:project) { merge_request.project }
before do
@@ -111,7 +111,7 @@
end
context 'closes related todos' do
- let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
let(:project) { merge_request.project }
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
let!(:todo) do
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
index a3b48abae2621f35f0fb49163bbeac749554e380..24d09c1fd00ef551802a77e1db6b01c79068dd5b 100644
--- a/spec/services/merge_requests/merge_to_ref_service_spec.rb
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -149,7 +149,7 @@ def process_merge_to_ref
end
context 'does not close related todos' do
- let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
let(:project) { merge_request.project }
let!(:todo) do
create(:todo, :assigned,
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 5ad6f5528f97331083a87c63469af40719c6583f..2cebefee5d6fabe057b42ec383ac32b716be1f95 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -2,7 +2,7 @@
describe MergeRequests::PostMergeService do
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 21e71509ed6ad6bdc87e940daaa70e534f5cf3fe..8b6db1ce33ea23af52ffa01815264be165d2b8d1 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -4,7 +4,7 @@
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, :closed, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 3a8614390bfd24b92f8beae7dcc3e65461643e6f..15aea97ff29ec23f9c95280d8a18793c7748b415 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -13,7 +13,7 @@
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
- assignee_id: user3.id,
+ assignee_ids: [user3.id],
source_project: project,
author: create(:user))
end
@@ -48,7 +48,7 @@ def update_merge_request(opts)
{
title: 'New title',
description: 'Also please fix',
- assignee_id: user2.id,
+ assignee_ids: [user.id],
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
@@ -71,7 +71,7 @@ def update_merge_request(opts)
it 'matches base expectations' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title')
- expect(@merge_request.assignee).to eq(user2)
+ expect(@merge_request.assignees).to match_array([user])
expect(@merge_request).to be_closed
expect(@merge_request.labels.count).to eq(1)
expect(@merge_request.labels.first.title).to eq(label.name)
@@ -106,7 +106,7 @@ def update_merge_request(opts)
note = find_note('assigned to')
expect(note).not_to be_nil
- expect(note.note).to include "assigned to #{user2.to_reference}"
+ expect(note.note).to include "assigned to #{user.to_reference} and unassigned #{user3.to_reference}"
end
it 'creates a resource label event' do
@@ -293,7 +293,7 @@ def update_merge_request(opts)
context 'when is reassigned' do
before do
- update_merge_request({ assignee: user2 })
+ update_merge_request({ assignee_ids: [user2.id] })
end
it 'marks previous assignee pending todos as done' do
@@ -387,7 +387,7 @@ def update_merge_request(opts)
context 'when the assignee changes' do
it 'updates open merge request counter for assignees when merge request is reassigned' do
- update_merge_request(assignee_id: user2.id)
+ update_merge_request(assignee_ids: [user2.id])
expect(user3.assigned_open_merge_requests_count).to eq 0
expect(user2.assigned_open_merge_requests_count).to eq 1
@@ -541,36 +541,36 @@ def update_merge_request(opts)
end
end
- context 'updating asssignee_id' do
+ context 'updating asssignee_ids' do
it 'does not update assignee when assignee_id is invalid' do
- merge_request.update(assignee_id: user.id)
+ merge_request.update(assignee_ids: [user.id])
- update_merge_request(assignee_id: -1)
+ update_merge_request(assignee_ids: [-1])
- expect(merge_request.reload.assignee).to eq(user)
+ expect(merge_request.reload.assignees).to eq([user])
end
it 'unassigns assignee when user id is 0' do
- merge_request.update(assignee_id: user.id)
+ merge_request.update(assignee_ids: [user.id])
- update_merge_request(assignee_id: 0)
+ update_merge_request(assignee_ids: [0])
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'saves assignee when user id is valid' do
- update_merge_request(assignee_id: user.id)
+ update_merge_request(assignee_ids: [user.id])
- expect(merge_request.assignee_id).to eq(user.id)
+ expect(merge_request.assignee_ids).to eq([user.id])
end
it 'does not update assignee_id when user cannot read issue' do
- non_member = create(:user)
- original_assignee = merge_request.assignee
+ non_member = create(:user)
+ original_assignees = merge_request.assignees
- update_merge_request(assignee_id: non_member.id)
+ update_merge_request(assignee_ids: [non_member.id])
- expect(merge_request.assignee_id).to eq(original_assignee.id)
+ expect(merge_request.reload.assignees).to eq(original_assignees)
end
context "when issuable feature is private" do
@@ -583,7 +583,7 @@ def update_merge_request(opts)
feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level"
project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
- expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee }
+ expect { update_merge_request(assignee_ids: [assignee]) }.not_to change { merge_request.reload.assignees }
end
end
end
@@ -619,7 +619,7 @@ def update_merge_request(opts)
end
it 'is allowed by a user that can push to the source and can update the merge request' do
- merge_request.update!(assignee: user)
+ merge_request.update!(assignees: [user])
source_project.add_developer(user)
update_merge_request(allow_collaboration: true, title: 'Updated title')
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 9ba4a11104a6e5850ed67a9c5a0f39f3769f9445..14c73852e650161d095a9cdb7678ba37f4aadbf3 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -125,11 +125,7 @@
shared_examples 'participating by assignee notification' do
it 'emails the participant' do
- if issuable.is_a?(Issue)
- issuable.assignees << participant
- else
- issuable.update_attribute(:assignee, participant)
- end
+ issuable.assignees << participant
notification_trigger
@@ -620,13 +616,13 @@
context "merge request diff note" do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignees: [user], author: create(:user)) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
before do
build_team(note.project)
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
end
describe '#new_note' do
@@ -637,7 +633,7 @@
notification.new_note(note)
expect(SentNotification.last(3).map(&:recipient).map(&:id))
- .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id)
+ .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id)
expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
end
@@ -1223,11 +1219,12 @@
let(:group) { create(:group) }
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
+ let(:assignee) { create(:user) }
+ let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee], description: 'cc @participant' }
before do
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -1239,7 +1236,7 @@
it do
notification.new_merge_request(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
@@ -1254,9 +1251,11 @@
it 'adds "assigned" reason for assignee, if any' do
notification.new_merge_request(merge_request, @u_disabled)
- email = find_email_for(merge_request.assignee)
+ merge_request.assignees.each do |assignee|
+ email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ end
end
it "emails any mentioned users with the mention level" do
@@ -1347,9 +1346,9 @@
end
it do
- notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, [assignee])
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(merge_request.author)
should_email(@u_watcher)
should_email(@u_participant_mentioned)
@@ -1365,17 +1364,19 @@
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, [assignee])
- email = find_email_for(merge_request.assignee)
+ merge_request.assignees.each do |assignee|
+ email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ end
end
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
- let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) }
end
end
@@ -1388,7 +1389,7 @@
it do
notification.push_to_merge_request(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
@@ -1430,7 +1431,7 @@
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
- should_not_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_not_email(assignee) }
should_not_email(merge_request.author)
should_not_email(@u_watcher)
should_not_email(@u_participant_mentioned)
@@ -1499,7 +1500,7 @@
it do
notification.close_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -1529,7 +1530,7 @@
it do
notification.merge_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -1581,7 +1582,7 @@
it do
notification.reopen_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1606,7 +1607,7 @@
it do
notification.resolve_all_discussions(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1850,8 +1851,8 @@ def create_member!
let(:guest) { create(:user) }
let(:developer) { create(:user) }
let(:assignee) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) }
- let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") }
+ let(:merge_request) { create(:merge_request, source_project: private_project, assignees: [assignee]) }
+ let(:merge_request1) { create(:merge_request, source_project: private_project, assignees: [assignee], description: "cc @#{guest.username}") }
let(:note) { create(:note, noteable: merge_request, project: private_project) }
before do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 6a8b194ea1d045005ebe994189ed09b7b634d0c0..ee2b01cffe7ccf327bc8b92cb4c57dcf0b98b6fa 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -16,7 +16,9 @@
let(:service) { described_class.new(project, developer) }
before do
- stub_licensed_features(multiple_issue_assignees: false)
+ stub_licensed_features(multiple_issue_assignees: false,
+ multiple_merge_request_assignees: false)
+
project.add_developer(developer)
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 13d7d7957033e989d566ff4a958f1dd7fc1fcdac..51c5a803dbdaeabb48959345cc81754219678156 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -166,8 +166,8 @@
end
end
- describe '.change_issue_assignees' do
- subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) }
+ describe '.change_issuable_assignees' do
+ subject { described_class.change_issuable_assignees(noteable, project, author, [assignee]) }
let(:assignee) { create(:user) }
let(:assignee1) { create(:user) }
@@ -180,7 +180,7 @@
def build_note(old_assignees, new_assignees)
issue.assignees = new_assignees
- described_class.change_issue_assignees(issue, project, author, old_assignees).note
+ described_class.change_issuable_assignees(issue, project, author, old_assignees).note
end
it_behaves_like 'a note with overridable created_at'
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 8631f3f9a33ffedc1b96f82d3600a4fd8efa8bfd..89411b2e908761f301eba35e413fdb7226c0f67b 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -272,28 +272,6 @@
end
end
- describe '#reassigned_issue' do
- it 'creates a pending todo for new assignee' do
- unassigned_issue.assignees << john_doe
- service.reassigned_issue(unassigned_issue, author)
-
- should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo if unassigned' do
- issue.assignees.destroy_all # rubocop: disable DestroyAll
-
- should_not_create_any_todo { service.reassigned_issue(issue, author) }
- end
-
- it 'creates a todo if new assignee is the current user' do
- unassigned_issue.assignees << john_doe
- service.reassigned_issue(unassigned_issue, john_doe)
-
- should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
- end
- end
-
describe '#mark_pending_todos_as_done' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
@@ -504,10 +482,60 @@
end
end
+ describe '#reassigned_issuable' do
+ shared_examples 'reassigned issuable' do
+ it 'creates a pending todo for new assignee' do
+ issuable_unassigned.assignees = [john_doe]
+ service.reassigned_issuable(issuable_unassigned, author)
+
+ should_create_todo(user: john_doe, target: issuable_unassigned, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ issuable_assigned.assignees = []
+
+ should_not_create_any_todo { service.reassigned_issuable(issuable_assigned, author) }
+ end
+
+ it 'creates a todo if new assignee is the current user' do
+ issuable_assigned.assignees = [john_doe]
+ service.reassigned_issuable(issuable_assigned, john_doe)
+
+ should_create_todo(user: john_doe, target: issuable_assigned, author: john_doe, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo for guests' do
+ service.reassigned_issuable(issuable_assigned, author)
+ should_not_create_todo(user: guest, target: issuable_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'does not create a directly addressed todo for guests' do
+ service.reassigned_issuable(addressed_issuable_assigned, author)
+ should_not_create_todo(user: guest, target: addressed_issuable_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+ end
+
+ context 'issuable is a merge request' do
+ it_behaves_like 'reassigned issuable' do
+ let(:issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:issuable_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) }
+ end
+ end
+
+ context 'issuable is an issue' do
+ it_behaves_like 'reassigned issuable' do
+ let(:issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:issuable_unassigned) { create(:issue, project: project, author: author, assignees: []) }
+ end
+ end
+ end
+
describe 'Merge Requests' do
- let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
- let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
- let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
+ let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) }
describe '#new_merge_request' do
it 'creates a pending todo if assigned' do
@@ -659,38 +687,6 @@
end
end
- describe '#reassigned_merge_request' do
- it 'creates a pending todo for new assignee' do
- mr_unassigned.update_attribute(:assignee, john_doe)
- service.reassigned_merge_request(mr_unassigned, author)
-
- should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo if unassigned' do
- mr_assigned.update_attribute(:assignee, nil)
-
- should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) }
- end
-
- it 'creates a todo if new assignee is the current user' do
- mr_assigned.update_attribute(:assignee, john_doe)
- service.reassigned_merge_request(mr_assigned, john_doe)
-
- should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo for guests' do
- service.reassigned_merge_request(mr_assigned, author)
- should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
- end
-
- it 'does not create a directly addressed todo for guests' do
- service.reassigned_merge_request(addressed_mr_assigned, author)
- should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
- end
- end
-
describe '#merge_merge_request' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 83f1495a1c65d151a9592eb57a891f10f5565795..450e76d5f589631571c928177c5d9da1c57ef0d5 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -78,7 +78,7 @@
end
context "for an merge request the user was assigned to" do
- let!(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
before do
service.execute(user)
@@ -91,7 +91,7 @@
it 'migrates the merge request so that it is "Unassigned"' do
migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
- expect(migrated_merge_request.assignee).to be_nil
+ expect(migrated_merge_request.assignees).to be_empty
end
end
end
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
index 4df80b4168a159c61e5ddf0eb20af089a2577cca..ab6687f1d074c96d48c6693bbd16202f5c055be7 100644
--- a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -46,9 +46,9 @@ def allow_gitaly_n_plus_1
allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
end
- let!(:merge_request1) { create(:merge_request, author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
- let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
+ let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
+ let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') }
+ let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') }
diff --git a/spec/support/shared_contexts/merge_request_create.rb b/spec/support/shared_contexts/merge_request_create.rb
new file mode 100644
index 0000000000000000000000000000000000000000..529f481c2b674b033add6d5900f1f45c8dee5cdc
--- /dev/null
+++ b/spec/support/shared_contexts/merge_request_create.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+shared_context 'merge request create context' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { target_project }
+ let!(:milestone) { create(:milestone, project: target_project) }
+ let!(:label) { create(:label, project: target_project) }
+ let!(:label2) { create(:label, project: target_project) }
+
+ before do
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
+
+ sign_in(user)
+ visit project_new_merge_request_path(target_project,
+ merge_request: {
+ source_project_id: source_project.id,
+ target_project_id: target_project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+end
diff --git a/spec/support/shared_contexts/merge_request_edit.rb b/spec/support/shared_contexts/merge_request_edit.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c84510ff47d0bdff5a7f4a850c5180570f43f2b7
--- /dev/null
+++ b/spec/support/shared_contexts/merge_request_edit.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+shared_context 'merge request edit context' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:milestone) { create(:milestone, project: target_project) }
+ let!(:label) { create(:label, project: target_project) }
+ let!(:label2) { create(:label, project: target_project) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { target_project }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project,
+ target_project: target_project,
+ source_branch: 'fix',
+ target_branch: 'master')
+ end
+
+ before do
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
+
+ sign_in(user)
+ visit edit_project_merge_request_path(target_project, merge_request)
+ end
+end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 7038a366144d25701fc1b6ecd34c773708eb3958..ec1b1754cf029f6bab2f2114f31362792c3c6b80 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -1,42 +1,17 @@
RSpec.shared_examples 'a creatable merge request' do
include WaitForRequests
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { target_project }
- let!(:milestone) { create(:milestone, project: target_project) }
- let!(:label) { create(:label, project: target_project) }
- let!(:label2) { create(:label, project: target_project) }
-
- before do
- source_project.add_maintainer(user)
- target_project.add_maintainer(user)
- target_project.add_maintainer(user2)
-
- sign_in(user)
- visit project_new_merge_request_path(
- target_project,
- merge_request: {
- source_project_id: source_project.id,
- target_project_id: target_project.id,
- source_branch: 'fix',
- target_branch: 'master'
- })
- end
-
it 'creates new merge request', :js do
- click_button 'Assignee'
+ find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
click_link user2.name
end
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
-
click_link 'Assign to me'
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index eef0327c9a6a166f27b4623d55ec29e4992910e5..a6121fcc50a2ffdc191942ad5ad38b8bfc4a0363 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -1,34 +1,10 @@
RSpec.shared_examples 'an editable merge request' do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:milestone) { create(:milestone, project: target_project) }
- let!(:label) { create(:label, project: target_project) }
- let!(:label2) { create(:label, project: target_project) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { target_project }
- let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: target_project,
- source_branch: 'fix',
- target_branch: 'master')
- end
-
- before do
- source_project.add_maintainer(user)
- target_project.add_maintainer(user)
- target_project.add_maintainer(user2)
-
- sign_in(user)
- visit edit_project_merge_request_path(target_project, merge_request)
- end
-
it 'updates merge request', :js do
- click_button 'Assignee'
+ find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
click_link user.name
end
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bab7963f06f9c7eb10eac85a605a44d3b05d3a7e
--- /dev/null
+++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+shared_examples 'multiple assignees merge request' do |action, save_button_title|
+ it "#{action} a MR with multiple assignees", :js do
+ find('.js-assignee-search').click
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ click_link user2.name
+ end
+
+ # Extra click needed in order to toggle the dropdown
+ find('.js-assignee-search').click
+
+ expect(all('input[name="merge_request[assignee_ids][]"]', visible: false).map(&:value))
+ .to match_array([user.id.to_s, user2.id.to_s])
+
+ page.within '.js-assignee-search' do
+ expect(page).to have_content "#{user2.name} + 1 more"
+ end
+
+ click_button save_button_title
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content '2 Assignees'
+
+ click_link 'Edit'
+
+ expect(page).to have_content user.name
+ expect(page).to have_content user2.name
+ end
+ end
+
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ # Closing dropdown to persist
+ click_link 'Edit'
+
+ expect(page).to have_content user2.name
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/assignees_filter_spec.rb b/spec/support/shared_examples/finders/assignees_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..782a2d97746a97985f6e7ceee090a33c8d20897e
--- /dev/null
+++ b/spec/support/shared_examples/finders/assignees_filter_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+shared_examples 'assignee ID filter' do
+ it 'returns issuables assigned to that user' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'assignee username filter' do
+ it 'returns issuables assigned to those users' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'no assignee filter' do
+ let(:params) { { assignee_id: 'None' } }
+
+ it 'returns issuables not assigned to any assignee' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_id] = 0
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_id] = 'none'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'any assignee filter' do
+ context '' do
+ let(:params) { { assignee_id: 'Any' } }
+
+ it 'returns issuables assigned to any assignee' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables assigned to any assignee' do
+ params[:assignee_id] = 'any'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+ end
+end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index fd0cbe52ef75a5711dfe1a59d8a7d7ef90f601d7..0a3a46210ed57dcc421e2d867898f2f92b37431b 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -17,7 +17,7 @@
source_project: forked_project,
target_project: project,
author: user,
- assignee: user,
+ assignees: [user],
milestone: milestone)
end
@@ -41,7 +41,7 @@
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
- expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
end
@@ -53,7 +53,7 @@
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
- expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index d9bda1a34148a47e2e49cc70e158f913fe7840a9..23cb319a202b140854e21857310d3cf57b734626 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -53,19 +53,6 @@ def preload_view_requirements
expect(rendered).not_to have_css('.cannot-be-merged')
end
end
-
- context 'when assignee is not allowed to merge' do
- it 'shows a warning icon' do
- reporter = create(:user)
- project.add_reporter(reporter)
- closed_merge_request.update(assignee_id: reporter.id)
- assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
-
- render
-
- expect(rendered).to have_css('.cannot-be-merged')
- end
- end
end
context 'when the merge request is closed' do