Commit f495925f authored by Dylan Griffith's avatar Dylan Griffith

Merge branch '212329-related-issues-to-core_services' into 'master'

[Related Issues to core] Move all Related Issues services to core RUN AS-IF-FOSS

See merge request gitlab-org/gitlab!39664
parents 791c2f62 5ab5e759
...@@ -305,6 +305,24 @@ class Issue < ApplicationRecord ...@@ -305,6 +305,24 @@ class Issue < ApplicationRecord
end end
end end
def related_issues(current_user, preload: nil)
related_issues = ::Issue
.select(['issues.*', 'issue_links.id AS issue_link_id',
'issue_links.link_type as issue_link_type_value',
'issue_links.target_id as issue_link_source_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
.preload(preload)
.reorder('issue_link_id')
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
current_user,
filters: { read_cross_project: cross_project_filter })
end
def can_be_worked_on? def can_be_worked_on?
!self.closed? && !self.project.forked? !self.closed? && !self.project.forked?
end end
...@@ -378,6 +396,15 @@ class Issue < ApplicationRecord ...@@ -378,6 +396,15 @@ class Issue < ApplicationRecord
author.id == User.support_bot.id author.id == User.support_bot.id
end end
def issue_link_type
return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)
type = IssueLink.link_types.key(issue_link_type_value) || IssueLink::TYPE_RELATES_TO
return type if issue_link_source_id == id
IssueLink.inverse_link_type(type)
end
private private
def ensure_metrics def ensure_metrics
......
# frozen_string_literal: true
class IssueLink < ApplicationRecord
include FromUnion
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
scope :for_source_issue, ->(issue) { where(source_id: issue.id) }
scope :for_target_issue, ->(issue) { where(target_id: issue.id) }
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
def self.inverse_link_type(type)
type
end
private
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
IssueLink.prepend_if_ee('EE::IssueLink')
...@@ -11,6 +11,7 @@ class SystemNoteMetadata < ApplicationRecord ...@@ -11,6 +11,7 @@ class SystemNoteMetadata < ApplicationRecord
close duplicate close duplicate
moved merge moved merge
label milestone label milestone
relate unrelate
].freeze ].freeze
ICON_TYPES = %w[ ICON_TYPES = %w[
...@@ -19,7 +20,7 @@ class SystemNoteMetadata < ApplicationRecord ...@@ -19,7 +20,7 @@ class SystemNoteMetadata < ApplicationRecord
title time_tracking branch milestone discussion task moved title time_tracking branch milestone discussion task moved
opened closed merged duplicate locked unlocked outdated opened closed merged duplicate locked unlocked outdated
tag due_date pinned_embed cherry_pick health_status approved unapproved tag due_date pinned_embed cherry_pick health_status approved unapproved
status alert_issue_added status alert_issue_added relate unrelate
].freeze ].freeze
validates :note, presence: true validates :note, presence: true
......
...@@ -42,13 +42,7 @@ module IssuableLinks ...@@ -42,13 +42,7 @@ module IssuableLinks
def create_links def create_links
objects = linkable_issuables(referenced_issuables) objects = linkable_issuables(referenced_issuables)
# it is important that this is not called after relate_issuables, as it relinks epic to the issuable
# see EpicLinks::EpicIssues#relate_issuables
affected_epics = affected_epics(objects)
link_issuables(objects) link_issuables(objects)
Epics::UpdateDatesService.new(affected_epics).execute unless affected_epics.blank?
end end
def link_issuables(target_issuables) def link_issuables(target_issuables)
...@@ -88,10 +82,6 @@ module IssuableLinks ...@@ -88,10 +82,6 @@ module IssuableLinks
references(extractor) references(extractor)
end end
def affected_epics(issues)
[]
end
def references(extractor) def references(extractor)
extractor.issues extractor.issues
end end
...@@ -121,3 +111,5 @@ module IssuableLinks ...@@ -121,3 +111,5 @@ module IssuableLinks
end end
end end
end end
IssuableLinks::CreateService.prepend_if_ee('EE::IssuableLinks::CreateService')
...@@ -6,9 +6,7 @@ module IssueLinks ...@@ -6,9 +6,7 @@ module IssueLinks
def relate_issuables(referenced_issue) def relate_issuables(referenced_issue)
link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue) link = IssueLink.find_or_initialize_by(source: issuable, target: referenced_issue)
if params[:link_type].present? set_link_type(link)
link.link_type = params[:link_type]
end
if link.changed? && link.save if link.changed? && link.save
create_notes(referenced_issue) create_notes(referenced_issue)
...@@ -32,5 +30,13 @@ module IssueLinks ...@@ -32,5 +30,13 @@ module IssueLinks
def previous_related_issuables def previous_related_issuables
@related_issues ||= issuable.related_issues(current_user).to_a @related_issues ||= issuable.related_issues(current_user).to_a
end end
private
def set_link_type(_link)
# EE only
end
end end
end end
IssueLinks::CreateService.prepend_if_ee('EE::IssueLinks::CreateService')
...@@ -12,6 +12,8 @@ module Issues ...@@ -12,6 +12,8 @@ module Issues
close_service.new(project, current_user, {}).execute(duplicate_issue) close_service.new(project, current_user, {}).execute(duplicate_issue)
duplicate_issue.update(duplicated_to: canonical_issue) duplicate_issue.update(duplicated_to: canonical_issue)
relate_two_issues(duplicate_issue, canonical_issue)
end end
private private
...@@ -23,7 +25,10 @@ module Issues ...@@ -23,7 +25,10 @@ module Issues
def create_issue_canonical_note(canonical_issue, duplicate_issue) def create_issue_canonical_note(canonical_issue, duplicate_issue)
SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue) SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
end end
def relate_two_issues(duplicate_issue, canonical_issue)
params = { target_issuable: canonical_issue }
IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
end
end end
end end
Issues::DuplicateService.prepend_if_ee('EE::Issues::DuplicateService')
...@@ -38,6 +38,7 @@ module Issues ...@@ -38,6 +38,7 @@ module Issues
def update_old_entity def update_old_entity
super super
rewrite_related_issues
mark_as_moved mark_as_moved
end end
...@@ -58,6 +59,14 @@ module Issues ...@@ -58,6 +59,14 @@ module Issues
original_entity.update(moved_to: new_entity) original_entity.update(moved_to: new_entity)
end end
def rewrite_related_issues
source_issue_links = IssueLink.for_source_issue(original_entity)
source_issue_links.update_all(source_id: new_entity.id)
target_issue_links = IssueLink.for_target_issue(original_entity)
target_issue_links.update_all(target_id: new_entity.id)
end
def notify_participants def notify_participants
notification_service.async.issue_moved(original_entity, new_entity, @current_user) notification_service.async.issue_moved(original_entity, new_entity, @current_user)
end end
......
...@@ -10,6 +10,7 @@ module QuickActions ...@@ -10,6 +10,7 @@ module QuickActions
include Gitlab::QuickActions::MergeRequestActions include Gitlab::QuickActions::MergeRequestActions
include Gitlab::QuickActions::CommitActions include Gitlab::QuickActions::CommitActions
include Gitlab::QuickActions::CommonActions include Gitlab::QuickActions::CommonActions
include Gitlab::QuickActions::RelateActions
attr_reader :quick_action_target attr_reader :quick_action_target
......
...@@ -45,6 +45,14 @@ module SystemNoteService ...@@ -45,6 +45,14 @@ module SystemNoteService
::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_milestone(milestone) ::SystemNotes::IssuablesService.new(noteable: noteable, project: project, author: author).change_milestone(milestone)
end end
def relate_issue(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).relate_issue(noteable_ref)
end
def unrelate_issue(noteable, noteable_ref, user)
::SystemNotes::IssuablesService.new(noteable: noteable, project: noteable.project, author: user).unrelate_issue(noteable_ref)
end
# Called when the due_date of a Noteable is changed # Called when the due_date of a Noteable is changed
# #
# noteable - Noteable object # noteable - Noteable object
......
...@@ -2,6 +2,34 @@ ...@@ -2,6 +2,34 @@
module SystemNotes module SystemNotes
class IssuablesService < ::SystemNotes::BaseService class IssuablesService < ::SystemNotes::BaseService
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "marked this issue as related to gitlab-foss#9001"
#
# Returns the created Note object
def relate_issue(noteable_ref)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
end
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "removed the relation with gitlab-foss#9001"
#
# Returns the created Note object
def unrelate_issue(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
end
# Called when the assignee of a Noteable is changed or removed # Called when the assignee of a Noteable is changed or removed
# #
# assignee - User being assigned, or nil # assignee - User being assigned, or nil
......
...@@ -22,4 +22,6 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do ...@@ -22,4 +22,6 @@ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
post :import_csv post :import_csv
post :export_csv post :export_csv
end end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end end
...@@ -143,24 +143,6 @@ module EE ...@@ -143,24 +143,6 @@ module EE
user&.can?(:admin_epic, project.group) user&.can?(:admin_epic, project.group)
end end
def related_issues(current_user, preload: nil)
related_issues = ::Issue
.select(['issues.*', 'issue_links.id AS issue_link_id',
'issue_links.link_type as issue_link_type_value',
'issue_links.target_id as issue_link_source_id'])
.joins("INNER JOIN issue_links ON
(issue_links.source_id = issues.id AND issue_links.target_id = #{id})
OR
(issue_links.target_id = issues.id AND issue_links.source_id = #{id})")
.preload(preload)
.reorder('issue_link_id')
cross_project_filter = -> (issues) { issues.where(project: project) }
Ability.issues_readable_by_user(related_issues,
current_user,
filters: { read_cross_project: cross_project_filter })
end
# Issue position on boards list should be relative to all group projects # Issue position on boards list should be relative to all group projects
def parent_ids def parent_ids
return super unless has_group_boards? return super unless has_group_boards?
...@@ -180,15 +162,6 @@ module EE ...@@ -180,15 +162,6 @@ module EE
!!promoted_to_epic_id !!promoted_to_epic_id
end end
def issue_link_type
return unless respond_to?(:issue_link_type_value) && respond_to?(:issue_link_source_id)
type = IssueLink.link_types.key(issue_link_type_value) || IssueLink::TYPE_RELATES_TO
return type if issue_link_source_id == id
IssueLink.inverse_link_type(type)
end
class_methods do class_methods do
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
...@@ -220,7 +193,7 @@ module EE ...@@ -220,7 +193,7 @@ module EE
end end
def update_blocking_issues_count! def update_blocking_issues_count!
blocking_count = IssueLink.blocking_issues_count_for(self) blocking_count = ::IssueLink.blocking_issues_count_for(self)
update!(blocking_issues_count: blocking_count) update!(blocking_issues_count: blocking_count)
end end
......
# frozen_string_literal: true
module EE
module IssueLink
extend ActiveSupport::Concern
prepended do
after_create :refresh_blocking_issue_cache
after_destroy :refresh_blocking_issue_cache
end
class_methods do
def inverse_link_type(type)
case type
when ::IssueLink::TYPE_BLOCKS
::IssueLink::TYPE_IS_BLOCKED_BY
when ::IssueLink::TYPE_IS_BLOCKED_BY
::IssueLink::TYPE_BLOCKS
else
type
end
end
def blocked_issue_ids(issue_ids)
blocked_and_blocking_issues_union(issue_ids).pluck(:blocked_issue_id)
end
def blocking_issue_ids_for(issue)
blocked_and_blocking_issues_union(issue.id).pluck(:blocking_issue_id)
end
def blocked_and_blocking_issues_union(issue_ids)
from_union([
blocked_or_blocking_issues(issue_ids, ::IssueLink::TYPE_BLOCKS),
blocked_or_blocking_issues(issue_ids, ::IssueLink::TYPE_IS_BLOCKED_BY)
])
end
def blocked_or_blocking_issues(issue_ids, link_type)
if link_type == ::IssueLink::TYPE_BLOCKS
blocked_key = :target_id
blocking_key = :source_id
else
blocked_key = :source_id
blocking_key = :target_id
end
select("#{blocked_key} as blocked_issue_id, #{blocking_key} as blocking_issue_id")
.where(link_type: link_type).where(blocked_key => issue_ids)
.joins("INNER JOIN issues ON issues.id = issue_links.#{blocking_key}")
.where('issues.state_id' => ::Issuable::STATE_ID_MAP[:opened])
end
def blocking_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocking_issue_id')
.joins(:target)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_BLOCKS)
.where(source_id: issues_ids)
.group(:blocking_issue_id),
select('COUNT(*), issue_links.target_id AS blocking_issue_id')
.joins(:source)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
.where(target_id: issues_ids)
.group(:blocking_issue_id)
], remove_duplicates: false).select('blocking_issue_id, SUM(count) AS count').group('blocking_issue_id')
end
def blocked_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocked_issue_id')
.joins(:target)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
.where(source_id: issues_ids)
.group(:blocked_issue_id),
select('COUNT(*), issue_links.target_id AS blocked_issue_id')
.joins(:source)
.where(issues: { state_id: ::Issue.available_states[:opened] })
.where(link_type: ::IssueLink::TYPE_BLOCKS)
.where(target_id: issues_ids)
.group(:blocked_issue_id)
], remove_duplicates: false).select('blocked_issue_id, SUM(count) AS count').group('blocked_issue_id')
end
def blocking_issues_count_for(issue)
blocking_issues_for_collection(issue.id)[0]&.count.to_i
end
end
private
def blocking_issue
case link_type
when ::IssueLink::TYPE_BLOCKS then source
when ::IssueLink::TYPE_IS_BLOCKED_BY then target
end
end
def refresh_blocking_issue_cache
blocking_issue&.update_blocking_issues_count!
end
end
end
...@@ -5,7 +5,7 @@ module EE ...@@ -5,7 +5,7 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
EE_ICON_TYPES = %w[ EE_ICON_TYPES = %w[
weight relate unrelate published weight published
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
epic_issue_moved issue_changed_epic epic_date_changed relate_epic unrelate_epic epic_issue_moved issue_changed_epic epic_date_changed relate_epic unrelate_epic
vulnerability_confirmed vulnerability_dismissed vulnerability_resolved vulnerability_confirmed vulnerability_dismissed vulnerability_resolved
...@@ -13,7 +13,6 @@ module EE ...@@ -13,7 +13,6 @@ module EE
].freeze ].freeze
EE_TYPES_WITH_CROSS_REFERENCES = %w[ EE_TYPES_WITH_CROSS_REFERENCES = %w[
relate unrelate
epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic epic_issue_added issue_added_to_epic epic_issue_removed issue_removed_from_epic
epic_issue_moved issue_changed_epic relate_epic unrelate_epic epic_issue_moved issue_changed_epic relate_epic unrelate_epic
iteration iteration
......
# frozen_string_literal: true
class IssueLink < ApplicationRecord
include FromUnion
belongs_to :source, class_name: 'Issue'
belongs_to :target, class_name: 'Issue'
validates :source, presence: true
validates :target, presence: true
validates :source, uniqueness: { scope: :target_id, message: 'is already related' }
validate :check_self_relation
scope :for_source_issue, ->(issue) { where(source_id: issue.id) }
scope :for_target_issue, ->(issue) { where(target_id: issue.id) }
TYPE_RELATES_TO = 'relates_to'
TYPE_BLOCKS = 'blocks'
TYPE_IS_BLOCKED_BY = 'is_blocked_by'
after_create :refresh_blocking_issue_cache
after_destroy :refresh_blocking_issue_cache
enum link_type: { TYPE_RELATES_TO => 0, TYPE_BLOCKS => 1, TYPE_IS_BLOCKED_BY => 2 }
class << self
def inverse_link_type(type)
case type
when TYPE_BLOCKS
TYPE_IS_BLOCKED_BY
when TYPE_IS_BLOCKED_BY
TYPE_BLOCKS
else
type
end
end
def blocked_issue_ids(issue_ids)
blocked_and_blocking_issues_union(issue_ids).pluck(:blocked_issue_id)
end
def blocking_issue_ids_for(issue)
blocked_and_blocking_issues_union(issue.id).pluck(:blocking_issue_id)
end
end
private
def blocking_issue
case link_type
when TYPE_BLOCKS then source
when TYPE_IS_BLOCKED_BY then target
end
end
def refresh_blocking_issue_cache
blocking_issue&.update_blocking_issues_count!
end
class << self
def blocked_and_blocking_issues_union(issue_ids)
from_union([
blocked_or_blocking_issues(issue_ids, IssueLink::TYPE_BLOCKS),
blocked_or_blocking_issues(issue_ids, IssueLink::TYPE_IS_BLOCKED_BY)
])
end
def blocked_or_blocking_issues(issue_ids, link_type)
if link_type == IssueLink::TYPE_BLOCKS
blocked_key = :target_id
blocking_key = :source_id
else
blocked_key = :source_id
blocking_key = :target_id
end
select("#{blocked_key} as blocked_issue_id, #{blocking_key} as blocking_issue_id")
.where(link_type: link_type).where(blocked_key => issue_ids)
.joins("INNER JOIN issues ON issues.id = issue_links.#{blocking_key}")
.where('issues.state_id' => Issuable::STATE_ID_MAP[:opened])
end
def blocking_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocking_issue_id')
.joins(:target)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_BLOCKS)
.where(source_id: issues_ids)
.group(:blocking_issue_id),
select('COUNT(*), issue_links.target_id AS blocking_issue_id')
.joins(:source)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_IS_BLOCKED_BY)
.where(target_id: issues_ids)
.group(:blocking_issue_id)
], remove_duplicates: false).select('blocking_issue_id, SUM(count) AS count').group('blocking_issue_id')
end
def blocked_issues_for_collection(issues_ids)
from_union([
select('COUNT(*), issue_links.source_id AS blocked_issue_id')
.joins(:target)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_IS_BLOCKED_BY)
.where(source_id: issues_ids)
.group(:blocked_issue_id),
select('COUNT(*), issue_links.target_id AS blocked_issue_id')
.joins(:source)
.where(issues: { state_id: Issue.available_states[:opened] })
.where(link_type: TYPE_BLOCKS)
.where(target_id: issues_ids)
.group(:blocked_issue_id)
], remove_duplicates: false).select('blocked_issue_id, SUM(count) AS count').group('blocked_issue_id')
end
def blocking_issues_count_for(issue)
blocking_issues_for_collection(issue.id)[0]&.count.to_i
end
end
def check_self_relation
return unless source && target
if source == target
errors.add(:source, 'cannot be related to itself')
end
end
end
...@@ -6,9 +6,6 @@ module EE ...@@ -6,9 +6,6 @@ module EE
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
prepended do prepended do
with_scope :subject
condition(:related_issues_disabled) { !@subject.feature_available?(:blocked_issues) }
with_scope :subject with_scope :subject
condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) } condition(:repository_mirrors_enabled) { @subject.feature_available?(:repository_mirrors) }
...@@ -190,11 +187,6 @@ module EE ...@@ -190,11 +187,6 @@ module EE
prevent :push_code prevent :push_code
end end
rule { related_issues_disabled }.policy do
prevent :read_issue_link
prevent :admin_issue_link
end
rule { ~group_timelogs_available }.prevent :read_group_timelogs rule { ~group_timelogs_available }.prevent :read_group_timelogs
rule { can?(:guest_access) & iterations_available }.enable :read_iteration rule { can?(:guest_access) & iterations_available }.enable :read_iteration
......
# frozen_string_literal: true
module EE
module IssuableLinks
module CreateService
extend ::Gitlab::Utils::Override
private
override :link_issuables
def link_issuables(objects)
# it is important that this is not called after relate_issuables, as it relinks epic to the issuable
# relate_issuables is called during the `super` portion of this method
# see EpicLinks::EpicIssues#relate_issuables
affected_epics = affected_epics(objects)
super
Epics::UpdateDatesService.new(affected_epics).execute unless affected_epics.blank?
end
def affected_epics(_issues)
[]
end
end
end
end
# frozen_string_literal: true
module EE
module IssueLinks
module CreateService
def execute
if params[:link_type].present?
return error('Blocked issues not available for current license', 403) unless link_type_available?
end
super
end
private
def set_link_type(link)
if params[:link_type].present?
link.link_type = params[:link_type]
end
end
def link_type_available?
return true unless [::IssueLink::TYPE_BLOCKS, ::IssueLink::TYPE_IS_BLOCKED_BY].include?(params[:link_type])
issuable.resource_parent.feature_available?(:blocked_issues)
end
end
end
end
# frozen_string_literal: true
module EE
module Issues
module DuplicateService
extend ::Gitlab::Utils::Override
override :execute
def execute(duplicate_issue, canonical_issue)
super
relate_two_issues(duplicate_issue, canonical_issue)
end
private
def relate_two_issues(duplicate_issue, canonical_issue)
params = { target_issuable: canonical_issue }
IssueLinks::CreateService.new(duplicate_issue, current_user, params).execute
end
end
end
end
...@@ -8,7 +8,6 @@ module EE ...@@ -8,7 +8,6 @@ module EE
override :update_old_entity override :update_old_entity
def update_old_entity def update_old_entity
rewrite_epic_issue rewrite_epic_issue
rewrite_related_issues
rewrite_related_vulnerability_issues rewrite_related_vulnerability_issues
super super
end end
...@@ -23,14 +22,6 @@ module EE ...@@ -23,14 +22,6 @@ module EE
original_entity.reset original_entity.reset
end end
def rewrite_related_issues
source_issue_links = IssueLink.for_source_issue(original_entity)
source_issue_links.update_all(source_id: new_entity.id)
target_issue_links = IssueLink.for_target_issue(original_entity)
target_issue_links.update_all(target_id: new_entity.id)
end
def rewrite_related_vulnerability_issues def rewrite_related_vulnerability_issues
issue_links = Vulnerabilities::IssueLink.for_issue(original_entity) issue_links = Vulnerabilities::IssueLink.for_issue(original_entity)
issue_links.update_all(issue_id: new_entity.id) issue_links.update_all(issue_id: new_entity.id)
......
...@@ -12,7 +12,6 @@ module EE ...@@ -12,7 +12,6 @@ module EE
include EE::Gitlab::QuickActions::IssueActions include EE::Gitlab::QuickActions::IssueActions
include EE::Gitlab::QuickActions::MergeRequestActions include EE::Gitlab::QuickActions::MergeRequestActions
include EE::Gitlab::QuickActions::IssueAndMergeRequestActions include EE::Gitlab::QuickActions::IssueAndMergeRequestActions
include EE::Gitlab::QuickActions::RelateActions
# rubocop: enable Cop/InjectEnterpriseEditionModule # rubocop: enable Cop/InjectEnterpriseEditionModule
end end
end end
......
...@@ -16,14 +16,6 @@ module EE ...@@ -16,14 +16,6 @@ module EE
extend_if_ee('EE::SystemNoteService') # rubocop: disable Cop/InjectEnterpriseEditionModule extend_if_ee('EE::SystemNoteService') # rubocop: disable Cop/InjectEnterpriseEditionModule
end end
def relate_issue(noteable, noteable_ref, user)
issuables_service(noteable, noteable.project, user).relate_issue(noteable_ref)
end
def unrelate_issue(noteable, noteable_ref, user)
issuables_service(noteable, noteable.project, user).unrelate_issue(noteable_ref)
end
def epic_issue(epic, issue, user, type) def epic_issue(epic, issue, user, type)
epics_service(epic, user).epic_issue(issue, type) epics_service(epic, user).epic_issue(issue, type)
end end
......
...@@ -2,34 +2,6 @@ ...@@ -2,34 +2,6 @@
module EE module EE
module SystemNotes module SystemNotes
module IssuablesService module IssuablesService
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "marked this issue as related to gitlab-foss#9001"
#
# Returns the created Note object
def relate_issue(noteable_ref)
body = "marked this issue as related to #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'relate'))
end
#
# noteable_ref - Referenced noteable object
#
# Example Note text:
#
# "removed the relation with gitlab-foss#9001"
#
# Returns the created Note object
def unrelate_issue(noteable_ref)
body = "removed the relation with #{noteable_ref.to_reference(noteable.project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'unrelate'))
end
# Called when the weight of a Noteable is changed # Called when the weight of a Noteable is changed
# #
# Example Note text: # Example Note text:
......
...@@ -5,6 +5,4 @@ resources :issues, only: [], constraints: { id: /\d+/ } do ...@@ -5,6 +5,4 @@ resources :issues, only: [], constraints: { id: /\d+/ } do
get '/descriptions/:version_id/diff', action: :description_diff, as: :description_diff get '/descriptions/:version_id/diff', action: :description_diff, as: :description_diff
delete '/descriptions/:version_id', action: :delete_description_version, as: :delete_description_version delete '/descriptions/:version_id', action: :delete_description_version, as: :delete_description_version
end end
resources :issue_links, only: [:index, :create, :destroy], as: 'links', path: 'links'
end end
...@@ -21,9 +21,9 @@ module EE ...@@ -21,9 +21,9 @@ module EE
def grouped_blocking_issues_count def grouped_blocking_issues_count
strong_memoize(:grouped_blocking_issues_count) do strong_memoize(:grouped_blocking_issues_count) do
next IssueLink.none unless collection_type == 'Issue' next ::IssueLink.none unless collection_type == 'Issue'
IssueLink.blocking_issues_for_collection(issuable_ids) ::IssueLink.blocking_issues_for_collection(issuable_ids)
end end
end end
end end
......
# frozen_string_literal: true
module EE
module Gitlab
module QuickActions
module RelateActions
extend ActiveSupport::Concern
include ::Gitlab::QuickActions::Dsl
included do
desc _('Mark this issue as related to another issue')
explanation do |related_reference|
_('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
execution_message do |related_reference|
_('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
params '#issue'
types Issue
condition do
quick_action_target.persisted? &&
current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
end
command :relate do |related_param|
IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_param] }).execute
end
end
end
end
end
end
...@@ -72,19 +72,6 @@ RSpec.describe 'Feature flag issue links', :js do ...@@ -72,19 +72,6 @@ RSpec.describe 'Feature flag issue links', :js do
expect(page).not_to have_selector '#related-issues' expect(page).not_to have_selector '#related-issues'
end end
end end
context 'when the related issues feature is unavailable' do
before do
stub_licensed_features(blocked_issues: false, feature_flags: true)
end
it 'does not show the related issues widget' do
visit(edit_project_feature_flag_path(project, feature_flag))
expect(page).to have_text 'Strategies'
expect(page).not_to have_selector '#related-issues'
end
end
end end
describe 'unlinking a feature flag from an issue' do describe 'unlinking a feature flag from an issue' do
......
...@@ -33,7 +33,9 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do ...@@ -33,7 +33,9 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do
end end
it 'does not make the query again' do it 'does not make the query again' do
expect(IssueLink).not_to receive(:blocked_issues_for_collection) # We cannot directly stub IssueLink, otherwise we get a strange RSpec error
issue_link = class_double('IssueLink').as_stubbed_const
expect(issue_link).not_to receive(:blocked_issues_for_collection)
subject.block_aggregate subject.block_aggregate
end end
...@@ -53,7 +55,9 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do ...@@ -53,7 +55,9 @@ RSpec.describe Gitlab::Graphql::Aggregations::Issues::LazyBlockAggregate do
end end
before do before do
expect(IssueLink).to receive(:blocked_issues_for_collection).and_return(fake_data) # We cannot directly stub IssueLink, otherwise we get a strange RSpec error
issue_link = class_double('IssueLink').as_stubbed_const
expect(issue_link).to receive(:blocked_issues_for_collection).and_return(fake_data)
end end
it 'clears the pending IDs' do it 'clears the pending IDs' do
......
...@@ -3,54 +3,6 @@ ...@@ -3,54 +3,6 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe IssueLink do RSpec.describe IssueLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Issue') }
it { is_expected.to belong_to(:target).class_name('Issue') }
end
describe 'link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
it 'provides the "related" as default link_type' do
expect(create(:issue_link).link_type).to eq 'relates_to'
end
end
describe 'Validation' do
subject { create :issue_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
context 'self relation' do
let(:issue) { create :issue }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
issue_link = build :issue_link, source: issue, target: nil
issue_link.valid?
expect(issue_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
issue_link = build :issue_link, source: issue, target: issue
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('cannot be related to itself')
end
end
end
end
context 'callbacks' do context 'callbacks' do
let_it_be(:target) { create(:issue) } let_it_be(:target) { create(:issue) }
let_it_be(:source) { create(:issue) } let_it_be(:source) { create(:issue) }
...@@ -61,7 +13,7 @@ RSpec.describe IssueLink do ...@@ -61,7 +13,7 @@ RSpec.describe IssueLink do
expect(source).to receive(:update_blocking_issues_count!) expect(source).to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!) expect(target).not_to receive(:update_blocking_issues_count!)
create(:issue_link, target: target, source: source, link_type: described_class::TYPE_BLOCKS) create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_BLOCKS)
end end
end end
...@@ -70,7 +22,7 @@ RSpec.describe IssueLink do ...@@ -70,7 +22,7 @@ RSpec.describe IssueLink do
expect(source).not_to receive(:update_blocking_issues_count!) expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).to receive(:update_blocking_issues_count!) expect(target).to receive(:update_blocking_issues_count!)
create(:issue_link, target: target, source: source, link_type: described_class::TYPE_IS_BLOCKED_BY) create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
end end
end end
...@@ -79,7 +31,7 @@ RSpec.describe IssueLink do ...@@ -79,7 +31,7 @@ RSpec.describe IssueLink do
expect(source).not_to receive(:update_blocking_issues_count!) expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!) expect(target).not_to receive(:update_blocking_issues_count!)
create(:issue_link, target: target, source: source, link_type: described_class::TYPE_RELATES_TO) create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_RELATES_TO)
end end
end end
end end
...@@ -87,7 +39,7 @@ RSpec.describe IssueLink do ...@@ -87,7 +39,7 @@ RSpec.describe IssueLink do
describe '.after_destroy_commit' do describe '.after_destroy_commit' do
context 'with TYPE_BLOCKS relation' do context 'with TYPE_BLOCKS relation' do
it 'updates blocking issues count' do it 'updates blocking issues count' do
link = create(:issue_link, target: target, source: source, link_type: described_class::TYPE_BLOCKS) link = create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_BLOCKS)
expect(source).to receive(:update_blocking_issues_count!) expect(source).to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!) expect(target).not_to receive(:update_blocking_issues_count!)
...@@ -98,7 +50,7 @@ RSpec.describe IssueLink do ...@@ -98,7 +50,7 @@ RSpec.describe IssueLink do
context 'with TYPE_IS_BLOCKED_BY' do context 'with TYPE_IS_BLOCKED_BY' do
it 'updates blocking issues count' do it 'updates blocking issues count' do
link = create(:issue_link, target: target, source: source, link_type: described_class::TYPE_IS_BLOCKED_BY) link = create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
expect(source).not_to receive(:update_blocking_issues_count!) expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).to receive(:update_blocking_issues_count!) expect(target).to receive(:update_blocking_issues_count!)
...@@ -109,7 +61,7 @@ RSpec.describe IssueLink do ...@@ -109,7 +61,7 @@ RSpec.describe IssueLink do
context 'with TYPE_RELATES_TO' do context 'with TYPE_RELATES_TO' do
it 'does not update blocking_issues_count' do it 'does not update blocking_issues_count' do
link = create(:issue_link, target: target, source: source, link_type: described_class::TYPE_RELATES_TO) link = create(:issue_link, target: target, source: source, link_type: ::IssueLink::TYPE_RELATES_TO)
expect(source).not_to receive(:update_blocking_issues_count!) expect(source).not_to receive(:update_blocking_issues_count!)
expect(target).not_to receive(:update_blocking_issues_count!) expect(target).not_to receive(:update_blocking_issues_count!)
...@@ -122,10 +74,10 @@ RSpec.describe IssueLink do ...@@ -122,10 +74,10 @@ RSpec.describe IssueLink do
describe '.blocked_issue_ids' do describe '.blocked_issue_ids' do
it 'returns only ids of issues which are blocked' do it 'returns only ids of issues which are blocked' do
link1 = create(:issue_link, link_type: described_class::TYPE_BLOCKS) link1 = create(:issue_link, link_type: ::IssueLink::TYPE_BLOCKS)
link2 = create(:issue_link, link_type: described_class::TYPE_IS_BLOCKED_BY) link2 = create(:issue_link, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
link3 = create(:issue_link, link_type: described_class::TYPE_RELATES_TO) link3 = create(:issue_link, link_type: ::IssueLink::TYPE_RELATES_TO)
link4 = create(:issue_link, source: create(:issue, :closed), link_type: described_class::TYPE_BLOCKS) link4 = create(:issue_link, source: create(:issue, :closed), link_type: ::IssueLink::TYPE_BLOCKS)
expect(described_class.blocked_issue_ids([link1.target_id, link2.source_id, link3.source_id, link4.target_id])) expect(described_class.blocked_issue_ids([link1.target_id, link2.source_id, link3.source_id, link4.target_id]))
.to match_array([link1.target_id, link2.source_id]) .to match_array([link1.target_id, link2.source_id])
...@@ -137,8 +89,8 @@ RSpec.describe IssueLink do ...@@ -137,8 +89,8 @@ RSpec.describe IssueLink do
issue = create(:issue) issue = create(:issue)
blocking_issue = create(:issue, project: issue.project) blocking_issue = create(:issue, project: issue.project)
blocked_by_issue = create(:issue, project: issue.project) blocked_by_issue = create(:issue, project: issue.project)
create(:issue_link, source: blocking_issue, target: issue, link_type: IssueLink::TYPE_BLOCKS) create(:issue_link, source: blocking_issue, target: issue, link_type: ::IssueLink::TYPE_BLOCKS)
create(:issue_link, source: issue, target: blocked_by_issue, link_type: IssueLink::TYPE_IS_BLOCKED_BY) create(:issue_link, source: issue, target: blocked_by_issue, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
blocking_ids = described_class.blocking_issue_ids_for(issue) blocking_ids = described_class.blocking_issue_ids_for(issue)
...@@ -163,9 +115,9 @@ RSpec.describe IssueLink do ...@@ -163,9 +115,9 @@ RSpec.describe IssueLink do
let_it_be(:blocking_issue_2) { create(:issue, project: project) } let_it_be(:blocking_issue_2) { create(:issue, project: project) }
before :all do before :all do
create(:issue_link, source: blocking_issue_1, target: blocked_issue_1, link_type: IssueLink::TYPE_BLOCKS) create(:issue_link, source: blocking_issue_1, target: blocked_issue_1, link_type: ::IssueLink::TYPE_BLOCKS)
create(:issue_link, source: blocked_issue_2, target: blocking_issue_1, link_type: IssueLink::TYPE_IS_BLOCKED_BY) create(:issue_link, source: blocked_issue_2, target: blocking_issue_1, link_type: ::IssueLink::TYPE_IS_BLOCKED_BY)
create(:issue_link, source: blocking_issue_2, target: blocked_issue_3, link_type: IssueLink::TYPE_BLOCKS) create(:issue_link, source: blocking_issue_2, target: blocked_issue_3, link_type: ::IssueLink::TYPE_BLOCKS)
end end
describe '.blocking_issues_for_collection' do describe '.blocking_issues_for_collection' do
......
...@@ -242,50 +242,6 @@ RSpec.describe Issue do ...@@ -242,50 +242,6 @@ RSpec.describe Issue do
let(:set_mentionable_text) { ->(txt) { subject.description = txt } } let(:set_mentionable_text) { ->(txt) { subject.description = txt } }
end end
describe '#related_issues' do
let(:user) { create(:user) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_a) { create(:issue, project: authorized_project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) }
before do
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
it 'returns only authorized related issues for given user' do
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b, authorized_issue_c)
end
it 'returns issues with valid issue_link_type' do
link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type)
expect(link_types).not_to be_empty
expect(link_types).not_to include(nil)
end
describe 'when a user cannot read cross project' do
it 'only returns issues within the same project' do
expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original
expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false)
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b)
end
end
end
describe '#allows_multiple_assignees?' do describe '#allows_multiple_assignees?' do
it 'does not allow multiple assignees without license' do it 'does not allow multiple assignees without license' do
stub_licensed_features(multiple_issue_assignees: false) stub_licensed_features(multiple_issue_assignees: false)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueLinks::CreateService do
describe '#execute' do
let(:namespace) { create :namespace }
let(:project) { create :project, namespace: namespace }
let(:issue) { create :issue, project: project }
let(:user) { create :user }
let(:params) do
{}
end
before do
stub_licensed_features(blocked_issues: true)
project.add_developer(user)
end
subject { described_class.new(issue, user, params).execute }
context 'when there is an issue to relate' do
let(:issue_a) { create :issue, project: project }
let(:another_project) { create :project, namespace: project.namespace }
let(:another_project_issue) { create :issue, project: another_project }
let(:issue_a_ref) { issue_a.to_reference }
let(:another_project_issue_ref) { another_project_issue.to_reference(project) }
let(:params) do
{ issuable_references: [issue_a_ref, another_project_issue_ref], link_type: 'is_blocked_by' }
end
before do
another_project.add_developer(user)
end
context 'when feature is not available' do
before do
stub_licensed_features(blocked_issues: false)
end
it 'returns error' do
is_expected.to eq(message: 'Blocked issues not available for current license', status: :error, http_status: 403)
end
it 'no relationship is created' do
expect { subject }.not_to change(IssueLink, :count)
end
end
it 'creates relationships' do
expect { subject }.to change(IssueLink, :count).from(0).to(2)
expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue, link_type: 'is_blocked_by')
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'is_blocked_by')
end
it 'returns success status' do
is_expected.to eq(status: :success)
end
end
context 'when reference of any already related issue is present' do
let(:issue_a) { create :issue, project: project }
let(:issue_b) { create :issue, project: project }
let(:issue_c) { create :issue, project: project }
before do
create :issue_link, source: issue, target: issue_b, link_type: IssueLink::TYPE_RELATES_TO
create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_IS_BLOCKED_BY
end
let(:params) do
{
issuable_references: [
issue_a.to_reference,
issue_b.to_reference,
issue_c.to_reference
],
link_type: IssueLink::TYPE_IS_BLOCKED_BY
}
end
it 'sets the same type of relation for selected references' do
expect(subject).to eq(status: :success)
expect(IssueLink.where(target: [issue_a, issue_b, issue_c]).pluck(:link_type))
.to eq([IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY])
end
end
end
end
...@@ -47,45 +47,6 @@ RSpec.describe Issues::MoveService do ...@@ -47,45 +47,6 @@ RSpec.describe Issues::MoveService do
end end
end end
describe '#rewrite_related_issues' do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:authorized_issue_d) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) }
let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) }
before do
stub_licensed_features(blocked_issues: true)
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
context 'multiple related issues' do
it 'moves all related issues and retains permissions' do
new_issue = move_service.execute(old_issue, new_project)
expect(new_issue.related_issues(admin))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue])
expect(new_issue.related_issues(user))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])
expect(authorized_issue_d.related_issues(user))
.to match_array([new_issue])
end
end
end
describe '#rewrite_related_vulnerability_issues' do describe '#rewrite_related_vulnerability_issues' do
let(:user) { create(:user) } let(:user) { create(:user) }
......
...@@ -312,48 +312,6 @@ RSpec.describe Notes::QuickActionsService do ...@@ -312,48 +312,6 @@ RSpec.describe Notes::QuickActionsService do
end end
end end
context '/relate' do
let(:other_issue) { create(:issue, project: project) }
let(:note_text) { "/relate #{other_issue.to_reference}" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
context 'user cannot relate issues' do
before do
project.update(visibility: Gitlab::VisibilityLevel::PUBLIC)
end
it 'does not create issue relation' do
expect { execute(note) }.not_to change { IssueLink.count }
end
end
context 'user is allowed to relate issues' do
before do
group.add_developer(user)
end
context 'related issues are not enabled' do
before do
stub_licensed_features(blocked_issues: false)
end
it 'does not create issue relation' do
expect { execute(note) }.not_to change { IssueLink.count }
end
end
context 'related issues are enabled' do
before do
stub_licensed_features(blocked_issues: true)
end
it 'creates issue relation' do
expect { execute(note) }.to change { IssueLink.count }.by(1)
end
end
end
end
context '/promote' do context '/promote' do
let(:note_text) { "/promote" } let(:note_text) { "/promote" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) } let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
......
...@@ -13,38 +13,6 @@ RSpec.describe ::SystemNotes::IssuablesService do ...@@ -13,38 +13,6 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) } let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
describe '#relate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.relate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'relate' }
end
context 'when issue marks another as related' do
it 'sets the note text' do
expect(subject.note).to eq "marked this issue as related to #{noteable_ref.to_reference(project)}"
end
end
end
describe '#unrelate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.unrelate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'unrelate' }
end
context 'when issue relation is removed' do
it 'sets the note text' do
expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
end
end
end
describe '#change_weight_note' do describe '#change_weight_note' do
context 'when weight changed' do context 'when weight changed' do
let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum', weight: 4) } let(:noteable) { create(:issue, project: project, title: 'Lorem ipsum', weight: 4) }
......
...@@ -879,101 +879,6 @@ RSpec.describe QuickActions::InterpretService do ...@@ -879,101 +879,6 @@ RSpec.describe QuickActions::InterpretService do
let(:issuable) { build(:merge_request, source_project: project) } let(:issuable) { build(:merge_request, source_project: project) }
end end
end end
context 'relate command' do
shared_examples 'relate command' do
it 'relates issues' do
service.execute(content, issue)
expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
end
end
context 'user is member of group' do
before do
group.add_developer(user)
end
context 'relate a single issue' do
let(:other_issue) { create(:issue, project: project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'relate multiple issues at once' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'empty relate command' do
let(:issues_related) { [] }
let(:content) { '/relate' }
it_behaves_like 'relate command'
end
context 'already having related issues' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{third_issue.to_reference(project)}" }
before do
create(:issue_link, source: issue, target: second_issue)
end
it_behaves_like 'relate command'
end
context 'cross project' do
let(:another_group) { create(:group, :public) }
let(:other_project) { create(:project, group: another_group) }
before do
another_group.add_developer(current_user)
end
context 'relate a cross project issue' do
let(:other_issue) { create(:issue, project: other_project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate multiple cross projects issues at once' do
let(:second_issue) { create(:issue, project: other_project) }
let(:third_issue) { create(:issue, project: other_project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate a non-existing issue' do
let(:issues_related) { [] }
let(:content) { "/relate imaginary##{non_existing_record_iid}" }
it_behaves_like 'relate command'
end
context 'relate a private issue' do
let(:private_project) { create(:project, :private) }
let(:other_issue) { create(:issue, project: private_project) }
let(:issues_related) { [] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
end
end
end
end end
describe '#explain' do describe '#explain' do
......
...@@ -14,40 +14,6 @@ RSpec.describe SystemNoteService do ...@@ -14,40 +14,6 @@ RSpec.describe SystemNoteService do
let_it_be(:issue) { noteable } let_it_be(:issue) { noteable }
let_it_be(:epic) { create(:epic, group: group) } let_it_be(:epic) { create(:epic, group: group) }
describe '.relate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:relate_issue).with(noteable_ref)
end
described_class.relate_issue(noteable, noteable_ref, double)
end
end
describe '.unrelate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:unrelate_issue).with(noteable_ref)
end
described_class.unrelate_issue(noteable, noteable_ref, double)
end
end
describe '.change_weight_note' do describe '.change_weight_note' do
it 'calls IssuableService' do it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service| expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
......
# frozen_string_literal: true
module Gitlab
module QuickActions
module RelateActions
extend ActiveSupport::Concern
include ::Gitlab::QuickActions::Dsl
included do
desc _('Mark this issue as related to another issue')
explanation do |related_reference|
_('Marks this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
execution_message do |related_reference|
_('Marked this issue as related to %{issue_ref}.') % { issue_ref: related_reference }
end
params '#issue'
types Issue
condition do
quick_action_target.persisted? &&
current_user.can?(:"update_#{quick_action_target.to_ability_name}", quick_action_target)
end
command :relate do |related_param|
IssueLinks::CreateService.new(quick_action_target, current_user, { issuable_references: [related_param] }).execute
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IssueLink do
describe 'Associations' do
it { is_expected.to belong_to(:source).class_name('Issue') }
it { is_expected.to belong_to(:target).class_name('Issue') }
end
describe 'link_type' do
it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1, is_blocked_by: 2) }
it 'provides the "related" as default link_type' do
expect(create(:issue_link).link_type).to eq 'relates_to'
end
end
describe 'Validation' do
subject { create :issue_link }
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:target) }
it do
is_expected.to validate_uniqueness_of(:source)
.scoped_to(:target_id)
.with_message(/already related/)
end
context 'self relation' do
let(:issue) { create :issue }
context 'cannot be validated' do
it 'does not invalidate object with self relation error' do
issue_link = build :issue_link, source: issue, target: nil
issue_link.valid?
expect(issue_link.errors[:source]).to be_empty
end
end
context 'can be invalidated' do
it 'invalidates object' do
issue_link = build :issue_link, source: issue, target: issue
expect(issue_link).to be_invalid
expect(issue_link.errors[:source]).to include('cannot be related to itself')
end
end
end
end
end
...@@ -311,6 +311,50 @@ RSpec.describe Issue do ...@@ -311,6 +311,50 @@ RSpec.describe Issue do
end end
end end
describe '#related_issues' do
let(:user) { create(:user) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_a) { create(:issue, project: authorized_project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: authorized_issue_a, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: authorized_issue_a, target: authorized_issue_c) }
before do
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
it 'returns only authorized related issues for given user' do
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b, authorized_issue_c)
end
it 'returns issues with valid issue_link_type' do
link_types = authorized_issue_a.related_issues(user).map(&:issue_link_type)
expect(link_types).not_to be_empty
expect(link_types).not_to include(nil)
end
describe 'when a user cannot read cross project' do
it 'only returns issues within the same project' do
expect(Ability).to receive(:allowed?).with(user, :read_all_resources, :global).at_least(:once).and_call_original
expect(Ability).to receive(:allowed?).with(user, :read_cross_project).and_return(false)
expect(authorized_issue_a.related_issues(user))
.to contain_exactly(authorized_issue_b)
end
end
end
describe '#can_move?' do describe '#can_move?' do
let(:issue) { create(:issue) } let(:issue) { create(:issue) }
......
...@@ -13,8 +13,6 @@ RSpec.describe IssueLinks::CreateService do ...@@ -13,8 +13,6 @@ RSpec.describe IssueLinks::CreateService do
end end
before do before do
stub_licensed_features(blocked_issues: true)
project.add_developer(user) project.add_developer(user)
end end
...@@ -87,7 +85,7 @@ RSpec.describe IssueLinks::CreateService do ...@@ -87,7 +85,7 @@ RSpec.describe IssueLinks::CreateService do
let(:another_project_issue_ref) { another_project_issue.to_reference(project) } let(:another_project_issue_ref) { another_project_issue.to_reference(project) }
let(:params) do let(:params) do
{ issuable_references: [issue_a_ref, another_project_issue_ref], link_type: 'is_blocked_by' } { issuable_references: [issue_a_ref, another_project_issue_ref] }
end end
before do before do
...@@ -97,8 +95,8 @@ RSpec.describe IssueLinks::CreateService do ...@@ -97,8 +95,8 @@ RSpec.describe IssueLinks::CreateService do
it 'creates relationships' do it 'creates relationships' do
expect { subject }.to change(IssueLink, :count).from(0).to(2) expect { subject }.to change(IssueLink, :count).from(0).to(2)
expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue, link_type: 'is_blocked_by') expect(IssueLink.find_by!(target: issue_a)).to have_attributes(source: issue, link_type: 'relates_to')
expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'is_blocked_by') expect(IssueLink.find_by!(target: another_project_issue)).to have_attributes(source: issue, link_type: 'relates_to')
end end
it 'returns success status' do it 'returns success status' do
...@@ -129,7 +127,7 @@ RSpec.describe IssueLinks::CreateService do ...@@ -129,7 +127,7 @@ RSpec.describe IssueLinks::CreateService do
before do before do
create :issue_link, source: issue, target: issue_b, link_type: IssueLink::TYPE_RELATES_TO create :issue_link, source: issue, target: issue_b, link_type: IssueLink::TYPE_RELATES_TO
create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_IS_BLOCKED_BY create :issue_link, source: issue, target: issue_c, link_type: IssueLink::TYPE_RELATES_TO
end end
let(:params) do let(:params) do
...@@ -139,27 +137,20 @@ RSpec.describe IssueLinks::CreateService do ...@@ -139,27 +137,20 @@ RSpec.describe IssueLinks::CreateService do
issue_b.to_reference, issue_b.to_reference,
issue_c.to_reference issue_c.to_reference
], ],
link_type: IssueLink::TYPE_IS_BLOCKED_BY link_type: IssueLink::TYPE_RELATES_TO
} }
end end
it 'creates notes only for new and changed relations' do it 'creates notes only for new relations' do
expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_a, anything) expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_a, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue_a, issue, anything) expect(SystemNoteService).to receive(:relate_issue).with(issue_a, issue, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue, issue_b, anything) expect(SystemNoteService).not_to receive(:relate_issue).with(issue, issue_b, anything)
expect(SystemNoteService).to receive(:relate_issue).with(issue_b, issue, anything) expect(SystemNoteService).not_to receive(:relate_issue).with(issue_b, issue, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue, issue_c, anything) expect(SystemNoteService).not_to receive(:relate_issue).with(issue, issue_c, anything)
expect(SystemNoteService).not_to receive(:relate_issue).with(issue_c, issue, anything) expect(SystemNoteService).not_to receive(:relate_issue).with(issue_c, issue, anything)
subject subject
end end
it 'sets the same type of relation for selected references' do
expect(subject).to eq(status: :success)
expect(IssueLink.where(target: [issue_a, issue_b, issue_c]).pluck(:link_type))
.to eq([IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY, IssueLink::TYPE_IS_BLOCKED_BY])
end
end end
context 'when there are invalid references' do context 'when there are invalid references' do
......
...@@ -9,8 +9,6 @@ RSpec.describe IssueLinks::ListService do ...@@ -9,8 +9,6 @@ RSpec.describe IssueLinks::ListService do
let(:user_role) { :developer } let(:user_role) { :developer }
before do before do
stub_licensed_features(blocked_issues: true)
project.add_role(user, user_role) project.add_role(user, user_role)
end end
......
...@@ -83,6 +83,17 @@ RSpec.describe Issues::DuplicateService do ...@@ -83,6 +83,17 @@ RSpec.describe Issues::DuplicateService do
expect(duplicate_issue.reload.duplicated_to).to eq(canonical_issue) expect(duplicate_issue.reload.duplicated_to).to eq(canonical_issue)
end end
it 'relates the duplicate issues' do
canonical_project.add_reporter(user)
duplicate_project.add_reporter(user)
subject.execute(duplicate_issue, canonical_issue)
issue_link = IssueLink.last
expect(issue_link.source).to eq(duplicate_issue)
expect(issue_link.target).to eq(canonical_issue)
end
end end
end end
end end
...@@ -223,6 +223,45 @@ RSpec.describe Issues::MoveService do ...@@ -223,6 +223,45 @@ RSpec.describe Issues::MoveService do
end end
end end
describe '#rewrite_related_issues' do
include_context 'user can move issue'
let(:admin) { create(:admin) }
let(:authorized_project) { create(:project) }
let(:authorized_project2) { create(:project) }
let(:unauthorized_project) { create(:project) }
let(:authorized_issue_b) { create(:issue, project: authorized_project) }
let(:authorized_issue_c) { create(:issue, project: authorized_project2) }
let(:authorized_issue_d) { create(:issue, project: authorized_project2) }
let(:unauthorized_issue) { create(:issue, project: unauthorized_project) }
let!(:issue_link_a) { create(:issue_link, source: old_issue, target: authorized_issue_b) }
let!(:issue_link_b) { create(:issue_link, source: old_issue, target: unauthorized_issue) }
let!(:issue_link_c) { create(:issue_link, source: old_issue, target: authorized_issue_c) }
let!(:issue_link_d) { create(:issue_link, source: authorized_issue_d, target: old_issue) }
before do
authorized_project.add_developer(user)
authorized_project2.add_developer(user)
end
context 'multiple related issues' do
it 'moves all related issues and retains permissions' do
new_issue = move_service.execute(old_issue, new_project)
expect(new_issue.related_issues(admin))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d, unauthorized_issue])
expect(new_issue.related_issues(user))
.to match_array([authorized_issue_b, authorized_issue_c, authorized_issue_d])
expect(authorized_issue_d.related_issues(user))
.to match_array([new_issue])
end
end
end
context 'updating sent notifications' do context 'updating sent notifications' do
let!(:old_issue_notification_1) { create(:sent_notification, project: old_issue.project, noteable: old_issue) } let!(:old_issue_notification_1) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
let!(:old_issue_notification_2) { create(:sent_notification, project: old_issue.project, noteable: old_issue) } let!(:old_issue_notification_2) { create(:sent_notification, project: old_issue.project, noteable: old_issue) }
......
...@@ -4,9 +4,9 @@ require 'spec_helper' ...@@ -4,9 +4,9 @@ require 'spec_helper'
RSpec.describe Notes::QuickActionsService do RSpec.describe Notes::QuickActionsService do
shared_context 'note on noteable' do shared_context 'note on noteable' do
let(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } } let_it_be(:maintainer) { create(:user).tap { |u| project.add_maintainer(u) } }
let(:assignee) { create(:user) } let_it_be(:assignee) { create(:user) }
before do before do
project.add_maintainer(assignee) project.add_maintainer(assignee)
...@@ -41,6 +41,36 @@ RSpec.describe Notes::QuickActionsService do ...@@ -41,6 +41,36 @@ RSpec.describe Notes::QuickActionsService do
end end
end end
context '/relate' do
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:other_issue) { create(:issue, project: project) }
let(:note_text) { "/relate #{other_issue.to_reference}" }
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
context 'user cannot relate issues' do
before do
project.team.find_member(maintainer.id).destroy!
project.update!(visibility: Gitlab::VisibilityLevel::PUBLIC)
end
it 'does not create issue relation' do
expect do
_, update_params = service.execute(note)
service.apply_updates(update_params, note)
end.not_to change { IssueLink.count }
end
end
context 'user is allowed to relate issues' do
it 'creates issue relation' do
expect do
_, update_params = service.execute(note)
service.apply_updates(update_params, note)
end.to change { IssueLink.count }.by(1)
end
end
end
describe '/reopen' do describe '/reopen' do
before do before do
note.noteable.close! note.noteable.close!
......
...@@ -1644,6 +1644,103 @@ RSpec.describe QuickActions::InterpretService do ...@@ -1644,6 +1644,103 @@ RSpec.describe QuickActions::InterpretService do
end end
end end
end end
context 'relate command' do
let_it_be_with_refind(:group) { create(:group) }
shared_examples 'relate command' do
it 'relates issues' do
service.execute(content, issue)
expect(IssueLink.where(source: issue).map(&:target)).to match_array(issues_related)
end
end
context 'user is member of group' do
before do
group.add_developer(developer)
end
context 'relate a single issue' do
let(:other_issue) { create(:issue, project: project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'relate multiple issues at once' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference} #{third_issue.to_reference}" }
it_behaves_like 'relate command'
end
context 'empty relate command' do
let(:issues_related) { [] }
let(:content) { '/relate' }
it_behaves_like 'relate command'
end
context 'already having related issues' do
let(:second_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{third_issue.to_reference(project)}" }
before do
create(:issue_link, source: issue, target: second_issue)
end
it_behaves_like 'relate command'
end
context 'cross project' do
let(:another_group) { create(:group, :public) }
let(:other_project) { create(:project, group: another_group) }
before do
another_group.add_developer(developer)
end
context 'relate a cross project issue' do
let(:other_issue) { create(:issue, project: other_project) }
let(:issues_related) { [other_issue] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate multiple cross projects issues at once' do
let(:second_issue) { create(:issue, project: other_project) }
let(:third_issue) { create(:issue, project: other_project) }
let(:issues_related) { [second_issue, third_issue] }
let(:content) { "/relate #{second_issue.to_reference(project)} #{third_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
context 'relate a non-existing issue' do
let(:issues_related) { [] }
let(:content) { "/relate imaginary##{non_existing_record_iid}" }
it_behaves_like 'relate command'
end
context 'relate a private issue' do
let(:private_project) { create(:project, :private) }
let(:other_issue) { create(:issue, project: private_project) }
let(:issues_related) { [] }
let(:content) { "/relate #{other_issue.to_reference(project)}" }
it_behaves_like 'relate command'
end
end
end
end
end end
describe '#explain' do describe '#explain' do
......
...@@ -86,6 +86,40 @@ RSpec.describe SystemNoteService do ...@@ -86,6 +86,40 @@ RSpec.describe SystemNoteService do
end end
end end
describe '.relate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:relate_issue).with(noteable_ref)
end
described_class.relate_issue(noteable, noteable_ref, double)
end
end
describe '.unrelate_issue' do
let(:noteable_ref) { double }
let(:noteable) { double }
before do
allow(noteable).to receive(:project).and_return(double)
end
it 'calls IssuableService' do
expect_next_instance_of(::SystemNotes::IssuablesService) do |service|
expect(service).to receive(:unrelate_issue).with(noteable_ref)
end
described_class.unrelate_issue(noteable, noteable_ref, double)
end
end
describe '.change_due_date' do describe '.change_due_date' do
let(:due_date) { double } let(:due_date) { double }
......
...@@ -13,6 +13,38 @@ RSpec.describe ::SystemNotes::IssuablesService do ...@@ -13,6 +13,38 @@ RSpec.describe ::SystemNotes::IssuablesService do
let(:service) { described_class.new(noteable: noteable, project: project, author: author) } let(:service) { described_class.new(noteable: noteable, project: project, author: author) }
describe '#relate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.relate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'relate' }
end
context 'when issue marks another as related' do
it 'sets the note text' do
expect(subject.note).to eq "marked this issue as related to #{noteable_ref.to_reference(project)}"
end
end
end
describe '#unrelate_issue' do
let(:noteable_ref) { create(:issue) }
subject { service.unrelate_issue(noteable_ref) }
it_behaves_like 'a system note' do
let(:action) { 'unrelate' }
end
context 'when issue relation is removed' do
it 'sets the note text' do
expect(subject.note).to eq "removed the relation with #{noteable_ref.to_reference(project)}"
end
end
end
describe '#change_assignee' do describe '#change_assignee' do
subject { service.change_assignee(assignee) } subject { service.change_assignee(assignee) }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment