提交 ca884980 编写于 作者: O Oswaldo Ferreira

[CE] Support multiple assignees for merge requests

Backports https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/10161
(code out of ee/ folder).
上级 225edb0d
......@@ -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(),
......
......@@ -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 {
</button>
</div>
<div class="value hide-collapsed">
<span
v-if="mergeNotAllowedTooltipMessage"
v-tooltip
:title="mergeNotAllowedTooltipMessage"
data-placement="left"
class="float-right cannot-be-merged"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-exclamation-triangle"></i>
</span>
<template v-if="hasNoUsers">
<span class="assign-yourself no-value">
No assignee
......
......@@ -498,6 +498,16 @@
flex: 1;
}
.issuable-meta {
.author-link {
display: inline-block;
}
.issuable-comments {
height: 18px;
}
}
.merge-request-title {
margin-bottom: 2px;
......
......@@ -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 {
......
......@@ -192,12 +192,7 @@ module IssuableActions
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
......
......@@ -190,15 +190,15 @@ module IssuableCollections
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
......@@ -20,7 +20,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
def merge_request_params_attributes
[
:allow_collaboration,
:assignee_id,
:description,
:force_remove_source_branch,
:lock_version,
......@@ -35,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
:title,
:discussion_locked,
label_ids: [],
assignee_ids: [],
update_task: [:index, :checked, :line_number, :line_source]
]
end
......
......@@ -439,22 +439,6 @@ class IssuableFinder
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 @@ class IssuableFinder
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?
......
......@@ -144,18 +144,4 @@ class IssuesFinder < IssuableFinder
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
......@@ -69,7 +69,7 @@ module BoardsHelper
end
def board_sidebar_user_data
dropdown_options = issue_assignees_dropdown_options
dropdown_options = assignees_dropdown_options('issue')
{
toggle: 'dropdown',
......
......@@ -17,8 +17,8 @@ module FormHelper
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,
......@@ -28,8 +28,8 @@ module FormHelper
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',
......@@ -39,5 +39,36 @@ module FormHelper
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
......@@ -15,11 +15,14 @@ module IssuablesHelper
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
......
......@@ -24,10 +24,12 @@ module Emails
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
......
......@@ -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,
......
# 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
......@@ -67,13 +67,6 @@ module Issuable
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 @@ module Issuable
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 @@ module Issuable
participant :author
participant :notes_with_associations
participant :assignees
strip_attributes :title
......@@ -270,6 +277,10 @@ module Issuable
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
......@@ -314,11 +325,7 @@ module Issuable
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)
......@@ -355,10 +362,18 @@ module Issuable
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
......
......@@ -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 @@ class Issue < ApplicationRecord
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}"
......
......@@ -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
......@@ -69,8 +70,7 @@ class MergeRequest < ApplicationRecord
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
......@@ -79,10 +79,6 @@ class MergeRequest < ApplicationRecord
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
......@@ -188,19 +184,14 @@ class MergeRequest < ApplicationRecord
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
......@@ -337,31 +328,6 @@ class MergeRequest < ApplicationRecord
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}"
......@@ -682,15 +648,6 @@ class MergeRequest < ApplicationRecord
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!
......@@ -1208,7 +1165,7 @@ class MergeRequest < ApplicationRecord
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)
......
......@@ -674,6 +674,10 @@ class Project < ApplicationRecord
{ 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
......
......@@ -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
# frozen_string_literal: true
class IssueSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees, using: API::Entities::UserBasic
end
# 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
# 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
......@@ -8,9 +8,9 @@ class MergeRequestSerializer < BaseSerializer
entity =
case opts[:serializer]
when 'sidebar'
MergeRequestSidebarBasicEntity
IssuableSidebarBasicEntity
when 'sidebar_extras'
IssuableSidebarExtrasEntity
MergeRequestSidebarExtrasEntity
when 'basic'
MergeRequestBasicEntity
else
......
# 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
# frozen_string_literal: true
class MergeRequestSidebarExtrasEntity < IssuableSidebarExtrasEntity
expose :assignees do |merge_request|
MergeRequestAssigneeEntity.represent(merge_request.assignees, merge_request: merge_request)
end
end
......@@ -34,14 +34,20 @@ class IssuableBaseService < BaseService
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 @@ class IssuableBaseService < BaseService
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)
......
......@@ -20,7 +20,7 @@ module Issues
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 @@ module Issues
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
......
......@@ -39,7 +39,7 @@ module Issues
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')
......
......@@ -49,9 +49,9 @@ module MergeRequests
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)
......
......@@ -24,13 +24,13 @@ module MergeRequests
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 @@ module MergeRequests
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 @@ module MergeRequests
)
end
end
# rubocop:enable Metrics/AbcSize
def handle_task_changes(merge_request)
todo_service.mark_pending_todos_as_done(merge_request, current_user)
......
......@@ -247,15 +247,15 @@ module NotificationRecipientService
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 @@ module NotificationRecipientService
# 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 @@ module NotificationRecipientService
# 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
......
......@@ -95,8 +95,8 @@ class NotificationService
# 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 @@ class NotificationService
issue,
current_user,
action: "reassign",
previous_assignee: previous_assignees
previous_assignees: previous_assignees
)
previous_assignee_ids = previous_assignees.map(&:id)
......@@ -140,7 +140,7 @@ class NotificationService
# 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 @@ class NotificationService
# 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
......
......@@ -69,7 +69,7 @@ module SystemNoteService
# 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 @@ module SystemNoteService
# "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 @@ module SystemNoteService
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
......
......@@ -49,12 +49,12 @@ class TodoService
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 @@ class TodoService
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
......
%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
......@@ -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)
......@@ -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)}
......
......@@ -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 %>
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -4,7 +4,7 @@
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
= assignees_label(@issue)
- if @issue.description
%div
......
......@@ -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 %>
......@@ -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 %>
......@@ -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 %>
......@@ -5,9 +5,9 @@
%p.details
!= merge_path_description(@merge_request, '&rarr;')
- 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
......
......@@ -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 %>
%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
%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
......@@ -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'}" %>
......@@ -46,7 +46,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
......
......@@ -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
......
......@@ -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]
......
- 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?
......
......@@ -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
......
- 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)
- 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 } })
......@@ -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) }
......
= 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)}"
......@@ -21,7 +21,7 @@ Gitlab::Seeder.quiet do
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
}
......
......@@ -663,7 +663,11 @@ module API
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
......
......@@ -20,6 +20,7 @@ module API
def self.update_params_at_least_one_of
%i[
assignee_id
assignee_ids
description
labels
milestone_id
......@@ -184,6 +185,7 @@ module API
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'
......@@ -231,6 +233,7 @@ module API
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
......@@ -334,6 +337,7 @@ module API
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)
......
......@@ -10,7 +10,7 @@ module Banzai
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
......
......@@ -20,11 +20,7 @@ module Gitlab
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
......
......@@ -34,7 +34,6 @@ module Gitlab
end
SAFE_HOOK_RELATIONS = %i[
assignee
labels
total_time_spent
].freeze
......@@ -51,7 +50,9 @@ module Gitlab
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)
......
......@@ -987,9 +987,6 @@ msgstr ""
msgid "Assigned Merge Requests"
msgstr ""
msgid "Assigned to :name"
msgstr ""
msgid "Assigned to me"
msgstr ""
......@@ -5474,9 +5471,6 @@ msgstr ""
msgid "No activities found"
msgstr ""
msgid "No assignee"
msgstr ""
msgid "No branches found"
msgstr ""
......@@ -5564,9 +5558,6 @@ msgstr ""
msgid "None"
msgstr ""
msgid "Not allowed to merge"
msgstr ""
msgid "Not available"
msgstr ""
......@@ -7277,9 +7268,6 @@ msgstr ""
msgid "Select an existing Kubernetes cluster or create a new one"
msgstr ""
msgid "Select assignee"
msgstr ""
msgid "Select branch/tag"
msgstr ""
......@@ -8860,9 +8848,6 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
msgid "Toggle sidebar"
msgstr ""
msgid "ToggleButton|Toggle Status: OFF"
msgstr ""
......@@ -9870,9 +9855,6 @@ msgstr ""
msgid "among other things"
msgstr ""
msgid "assign yourself"
msgstr ""
msgid "attach a new file"
msgstr ""
......
......@@ -26,7 +26,7 @@ module QA
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
......
......@@ -238,11 +238,11 @@ describe Projects::MergeRequestsController do
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
......
......@@ -8,7 +8,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
before do
issue.assignees = [user]
merge_request.update(assignee: user)
merge_request.update(assignees: [user])
sign_in(user)
end
......@@ -33,7 +33,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
expect_counters('merge_requests', '1')
merge_request.update(assignee: nil)
merge_request.update(assignees: [])
user.invalidate_cache_counts
......
......@@ -48,14 +48,14 @@ describe 'Dashboard Merge Requests' do
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
......
......@@ -38,7 +38,7 @@ describe 'Group merge requests page' do
context 'when merge request assignee to user' do
before do
issuable.update!(assignee: user)
issuable.update!(assignees: [user])
visit path
end
......
......@@ -30,8 +30,8 @@ describe 'New/edit issue', :js do
# 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 = FormHelper.instance_method(:issue_assignees_dropdown_options)
allow_any_instance_of(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
......
......@@ -68,15 +68,15 @@ describe "User creates a merge request", :js do
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")
......
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 @@ describe 'Merge request > User creates MR' do
end
context 'to canonical project' do
include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
......@@ -25,6 +33,7 @@ describe 'Merge request > User creates MR' do
namespace: user.namespace)
end
include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
end
......
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
......@@ -7,7 +7,7 @@ describe 'Merge Requests > User filters by assignees', :js do
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)
......
......@@ -10,7 +10,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
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)
......
......@@ -12,7 +12,7 @@ describe 'Merge requests > User lists merge requests' do
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 @@ describe 'Merge requests > User lists merge requests' do
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)
......
......@@ -54,8 +54,7 @@ describe 'Merge requests > User mass updates', :js do
describe 'remove assignee' do
before do
merge_request.assignee = user
merge_request.save
merge_request.assignees = [user]
visit project_merge_requests_path(project)
end
......
......@@ -36,7 +36,7 @@ describe 'User uses header search field' do
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 @@ describe 'User uses header search field' do
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')
......
......@@ -13,60 +13,32 @@ describe IssuesFinder do
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
......
......@@ -136,21 +136,50 @@ describe MergeRequestsFinder do
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
......
......@@ -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" ]
......
......@@ -64,6 +64,11 @@
},
"additionalProperties": false
},
"assignees": {
"items": {
"$ref": "./merge_request.json"
}
},
"source_project_id": { "type": "integer" },
"target_project_id": { "type": "integer" },
"labels": {
......
......@@ -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({
......
......@@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do
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
......
......@@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do
it 'includes safe attribute' do
%w[
assignee_id
assignee_ids
author_id
created_at
description
......
......@@ -102,6 +102,7 @@ merge_requests:
- merge_request_pipelines
- merge_request_assignees
- suggestions
- assignees
merge_request_diff:
- merge_request
- merge_request_diff_commits
......@@ -336,6 +337,9 @@ push_event_payload:
issue_assignees:
- issue
- assignee
merge_request_assignees:
- merge_request
- assignee
lfs_file_locks:
- user
project_badges:
......
......@@ -621,3 +621,7 @@ Suggestion:
- outdated
- lines_above
- lines_below
MergeRequestAssignee:
- id
- user_id
- merge_request_id
......@@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do
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 @@ describe Gitlab::IssuableMetadata do
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) }
......
......@@ -19,7 +19,7 @@ describe Notify do
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
assignee: assignee,
assignees: [assignee],
description: 'Awesome description')
end
......@@ -275,7 +275,7 @@ describe Notify do
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 @@ describe Notify do
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 Notify do
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 @@ describe Notify do
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 @@ describe Notify do
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 @@ describe Notify do
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 @@ describe Notify do
source_project: project,
target_project: project,
author: current_user,
assignee: assignee,
assignees: [assignee],
description: 'Awesome description')
end
......
......@@ -684,12 +684,12 @@ describe Ci::Pipeline, :mailer do
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 @@ describe Ci::Pipeline, :mailer do
'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 @@ describe Ci::Pipeline, :mailer do
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')
......
# 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
......@@ -502,8 +502,8 @@ describe Issuable do
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 @@ describe Issuable do
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] })
......
......@@ -263,7 +263,7 @@ describe Event do
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 }
......
......@@ -13,7 +13,7 @@ describe MergeRequest do
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 @@ describe MergeRequest do
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 @@ describe MergeRequest do
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 @@ describe MergeRequest do
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 @@ describe MergeRequest do
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)
......
......@@ -2816,9 +2816,9 @@ describe User do
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
......
......@@ -270,8 +270,8 @@ describe API::Events do
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)
......
......@@ -5,14 +5,15 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
state: 'closed',
milestone: milestone1,
author: user,
assignee: user,
assignees: [user],
source_project: project,
target_project: project,
title: "Test",
......@@ -451,7 +455,7 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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 @@ describe API::MergeRequests do
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
......@@ -1574,6 +1643,19 @@ describe API::MergeRequests do
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),
......@@ -1728,7 +1810,7 @@ describe API::MergeRequests do
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)
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册