Commit 5ab5e759 authored by Mario de la Ossa's avatar Mario de la Ossa

Move all Related Issues services and models to core

As part of making simple related issues free
parent d795ab07
...@@ -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
...@@ -142,24 +142,6 @@ module EE ...@@ -142,24 +142,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?
...@@ -179,15 +161,6 @@ module EE ...@@ -179,15 +161,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
...@@ -219,7 +192,7 @@ module EE ...@@ -219,7 +192,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
......
...@@ -241,50 +241,6 @@ RSpec.describe Issue do ...@@ -241,50 +241,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