2018-07-25 05:30:33 -04:00
# frozen_string_literal: true
2014-05-23 04:22:00 -04:00
require 'carrierwave/orm/activerecord'
2019-03-28 09:17:42 -04:00
class Issue < ApplicationRecord
2018-03-06 14:09:01 -05:00
include AtomicInternalId
2018-05-11 03:52:48 -04:00
include IidRoutes
2015-05-02 23:11:21 -04:00
include Issuable
2017-03-09 20:29:11 -05:00
include Noteable
2015-05-02 23:11:21 -04:00
include Referable
2016-07-21 19:11:53 -04:00
include Spammable
2016-08-08 10:18:13 -04:00
include FasterCacheKeys
2017-02-01 13:41:01 -05:00
include RelativePositioning
2017-11-01 13:35:14 -04:00
include TimeTrackable
2017-12-01 08:52:16 -05:00
include ThrottledTouch
2018-08-01 04:58:49 -04:00
include LabelEventable
2019-12-01 01:06:11 -05:00
include IgnorableColumns
2020-02-19 13:09:10 -05:00
include MilestoneEventable
2020-04-09 20:10:04 -04:00
include WhereComposite
2020-05-11 11:09:37 -04:00
include StateEventable
2020-09-02 08:10:35 -04:00
include IdInOrdered
2020-09-25 11:09:36 -04:00
include Presentable
2020-09-30 14:09:52 -04:00
include IssueAvailableFeatures
2020-10-30 14:08:56 -04:00
include Todoable
2020-12-13 22:10:06 -05:00
include FromUnion
2021-06-16 08:10:18 -04:00
include EachBatch
2017-12-11 08:25:27 -05:00
2021-03-24 08:09:32 -04:00
extend :: Gitlab :: Utils :: Override
2018-05-31 10:01:04 -04:00
DueDateStruct = Struct . new ( :title , :name ) . freeze
NoDueDate = DueDateStruct . new ( 'No Due Date' , '0' ) . freeze
AnyDueDate = DueDateStruct . new ( 'Any Due Date' , '' ) . freeze
Overdue = DueDateStruct . new ( 'Overdue' , 'overdue' ) . freeze
DueThisWeek = DueDateStruct . new ( 'Due This Week' , 'week' ) . freeze
DueThisMonth = DueDateStruct . new ( 'Due This Month' , 'month' ) . freeze
DueNextMonthAndPreviousTwoWeeks = DueDateStruct . new ( 'Due Next Month And Previous Two Weeks' , 'next_month_and_previous_two_weeks' ) . freeze
2016-03-10 09:26:56 -05:00
2019-01-06 19:00:48 -05:00
SORTING_PREFERENCE_FIELD = :issues_sort
2020-09-17 05:09:32 -04:00
# Types of issues that should be displayed on lists across the app
# for example, project issues list, group issues list and issue boards.
# Some issue types, like test cases, should be hidden by default.
TYPES_FOR_LIST = %w( issue incident ) . freeze
2013-04-25 10:15:33 -04:00
belongs_to :project
2020-07-23 14:10:06 -04:00
has_one :namespace , through : :project
2019-09-13 09:26:31 -04:00
belongs_to :duplicated_to , class_name : 'Issue'
2018-02-09 13:23:55 -05:00
belongs_to :closed_by , class_name : 'User'
2020-05-14 08:08:21 -04:00
belongs_to :iteration , foreign_key : 'sprint_id'
2021-12-22 07:16:22 -05:00
belongs_to :work_item_type , class_name : 'WorkItems::Type' , inverse_of : :work_items
2016-03-17 05:31:17 -04:00
2020-04-23 14:09:46 -04:00
belongs_to :moved_to , class_name : 'Issue'
has_one :moved_from , class_name : 'Issue' , foreign_key : :moved_to_id
2020-11-10 19:08:58 -05:00
has_internal_id :iid , scope : :project , track_if : - > { ! importing? }
2018-03-12 10:38:56 -04:00
2019-09-06 05:54:58 -04:00
has_many :events , as : :target , dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
2016-07-01 09:34:10 -04:00
2017-06-08 11:16:27 -04:00
has_many :merge_requests_closing_issues ,
class_name : 'MergeRequestsClosingIssues' ,
dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
2020-10-02 08:09:03 -04:00
has_many :issue_email_participants
2021-11-24 10:14:19 -05:00
has_one :email
2018-04-18 09:41:42 -04:00
has_many :assignees , class_name : " User " , through : :issue_assignees
2019-10-28 14:06:15 -04:00
has_many :zoom_meetings
2020-02-13 19:09:07 -05:00
has_many :user_mentions , class_name : " IssueUserMention " , dependent : :delete_all # rubocop:disable Cop/ActiveRecordDependent
2020-02-27 19:09:08 -05:00
has_many :sent_notifications , as : :noteable
2020-05-04 02:10:10 -04:00
has_many :designs , class_name : 'DesignManagement::Design' , inverse_of : :issue
has_many :design_versions , class_name : 'DesignManagement::Version' , inverse_of : :issue do
def most_recent
ordered . first
end
end
2020-02-27 19:09:08 -05:00
2020-09-07 08:08:27 -04:00
has_one :issuable_severity
2019-12-10 07:07:55 -05:00
has_one :sentry_issue
2020-04-22 20:09:41 -04:00
has_one :alert_management_alert , class_name : 'AlertManagement::Alert'
2021-08-18 02:11:01 -04:00
has_one :incident_management_issuable_escalation_status , class_name : 'IncidentManagement::IssuableEscalationStatus'
2020-06-05 20:08:18 -04:00
has_and_belongs_to_many :self_managed_prometheus_alert_events , join_table : :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events , join_table : :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts , through : :prometheus_alert_events
2021-10-19 11:12:08 -04:00
has_many :issue_customer_relations_contacts , class_name : 'CustomerRelations::IssueContact' , inverse_of : :issue
has_many :customer_relations_contacts , through : :issue_customer_relations_contacts , source : :contact , class_name : 'CustomerRelations::Contact' , inverse_of : :issues
Improve performance of the cycle analytics page.
1. These changes bring down page load time for 100 issues from more than
a minute to about 1.5 seconds.
2. This entire commit is composed of these types of performance
enhancements:
- Cache relevant data in `IssueMetrics` wherever possible.
- Cache relevant data in `MergeRequestMetrics` wherever possible.
- Preload metrics
3. Given these improvements, we now only need to make 4 SQL calls:
- Load all issues
- Load all merge requests
- Load all metrics for the issues
- Load all metrics for the merge requests
4. A list of all the data points that are now being pre-calculated:
a. The first time an issue is mentioned in a commit
- In `GitPushService`, find all issues mentioned by the given commit
using `ReferenceExtractor`. Set the `first_mentioned_in_commit_at`
flag for each of them.
- There seems to be a (pre-existing) bug here - files (and
therefore commits) created using the Web CI don't have
cross-references created, and issues are not closed even when
the commit title is "Fixes #xx".
b. The first time a merge request is deployed to production
When a `Deployment` is created, find all merge requests that
were merged in before the deployment, and set the
`first_deployed_to_production_at` flag for each of them.
c. The start / end time for a merge request pipeline
Hook into the `Pipeline` state machine. When the `status` moves to
`running`, find the merge requests whose tip commit matches the
pipeline, and record the `latest_build_started_at` time for each
of them. When the `status` moves to `success`, record the
`latest_build_finished_at` time.
d. The merge requests that close an issue
- This was a big cause of the performance problems we were having
with Cycle Analytics. We need to use `ReferenceExtractor` to make
this calculation, which is slow when we have to run it on a large
number of merge requests.
- When a merge request is created, updated, or refreshed, find the
issues it closes, and create an instance of
`MergeRequestsClosingIssues`, which acts as a join model between
merge requests and issues.
- If a `MergeRequestsClosingIssues` instance links a merge request
and an issue, that issue closes that merge request.
5. The `Queries` module was changed into a class, so we can cache the
results of `issues` and `merge_requests_closing_issues` across
various cycle analytics stages.
6. The code added in this commit is untested. Tests will be added in the
next commit.
2016-09-15 04:59:36 -04:00
2021-12-20 13:13:27 -05:00
alias_attribute :escalation_status , :incident_management_issuable_escalation_status
2021-07-01 14:07:29 -04:00
accepts_nested_attributes_for :issuable_severity , update_only : true
2019-12-12 22:07:50 -05:00
accepts_nested_attributes_for :sentry_issue
2021-12-20 13:13:27 -05:00
accepts_nested_attributes_for :incident_management_issuable_escalation_status , update_only : true
2019-12-12 22:07:50 -05:00
2013-04-25 10:15:33 -04:00
validates :project , presence : true
2020-07-23 23:09:19 -04:00
validates :issue_type , presence : true
2021-12-22 07:16:22 -05:00
enum issue_type : WorkItems :: Type . base_types
2013-04-25 10:15:33 -04:00
2019-08-08 02:25:20 -04:00
alias_method :issuing_parent , :project
2018-01-04 07:29:48 -05:00
2020-11-27 19:09:43 -05:00
alias_attribute :external_author , :service_desk_reply_to
2016-01-22 04:24:38 -05:00
scope :in_projects , - > ( project_ids ) { where ( project_id : project_ids ) }
2020-10-12 14:08:31 -04:00
scope :not_in_projects , - > ( project_ids ) { where . not ( project_id : project_ids ) }
2013-02-19 04:01:19 -05:00
2018-06-28 08:48:10 -04:00
scope :with_due_date , - > { where . not ( due_date : nil ) }
2016-04-20 08:41:50 -04:00
scope :without_due_date , - > { where ( due_date : nil ) }
scope :due_before , - > ( date ) { where ( 'issues.due_date < ?' , date ) }
scope :due_between , - > ( from_date , to_date ) { where ( 'issues.due_date >= ?' , from_date ) . where ( 'issues.due_date <= ?' , to_date ) }
2018-03-30 09:05:20 -04:00
scope :due_tomorrow , - > { where ( due_date : Date . tomorrow ) }
2020-05-15 14:07:52 -04:00
scope :not_authored_by , - > ( user ) { where . not ( author_id : user ) }
2016-04-20 08:41:50 -04:00
2019-11-06 07:06:17 -05:00
scope :order_due_date_asc , - > { reorder ( :: Gitlab :: Database . nulls_last_order ( 'due_date' , 'ASC' ) ) }
scope :order_due_date_desc , - > { reorder ( :: Gitlab :: Database . nulls_last_order ( 'due_date' , 'DESC' ) ) }
scope :order_closest_future_date , - > { reorder ( Arel . sql ( 'CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC' ) ) }
2020-03-11 08:09:26 -04:00
scope :order_closed_date_desc , - > { reorder ( closed_at : :desc ) }
2020-03-18 11:09:45 -04:00
scope :order_created_at_desc , - > { reorder ( created_at : :desc ) }
2020-09-25 05:09:40 -04:00
scope :order_severity_asc , - > { includes ( :issuable_severity ) . order ( 'issuable_severities.severity ASC NULLS FIRST' ) }
scope :order_severity_desc , - > { includes ( :issuable_severity ) . order ( 'issuable_severities.severity DESC NULLS LAST' ) }
2016-04-19 07:10:25 -04:00
2020-03-24 14:07:55 -04:00
scope :preload_associated_models , - > { preload ( :assignees , :labels , project : :namespace ) }
2021-04-11 23:09:13 -04:00
scope :with_web_entity_associations , - > { preload ( :author , project : [ :project_feature , :route , namespace : :route ] ) }
2021-04-13 11:11:24 -04:00
scope :preload_awardable , - > { preload ( :award_emoji ) }
2020-05-15 05:07:59 -04:00
scope :with_label_attributes , - > ( label_attributes ) { joins ( :labels ) . where ( labels : label_attributes ) }
2020-06-05 20:08:18 -04:00
scope :with_alert_management_alerts , - > { joins ( :alert_management_alert ) }
scope :with_prometheus_alert_events , - > { joins ( :issues_prometheus_alert_events ) }
scope :with_self_managed_prometheus_alert_events , - > { joins ( :issues_self_managed_prometheus_alert_events ) }
2020-07-21 20:09:26 -04:00
scope :with_api_entity_associations , - > {
2021-05-06 05:18:56 -04:00
preload ( :timelogs , :closed_by , :assignees , :author , :labels ,
2020-07-21 20:09:26 -04:00
milestone : { project : [ :route , { namespace : :route } ] } ,
project : [ :route , { namespace : :route } ] )
}
2020-08-04 20:09:52 -04:00
scope :with_issue_type , - > ( types ) { where ( issue_type : types ) }
2021-09-20 02:09:32 -04:00
scope :without_issue_type , - > ( types ) { where . not ( issue_type : types ) }
2016-12-19 16:26:15 -05:00
2021-09-16 17:11:39 -04:00
scope :public_only , - > {
without_hidden . where ( confidential : false )
}
2019-02-25 06:00:24 -05:00
scope :confidential_only , - > { where ( confidential : true ) }
2017-08-17 11:21:25 -04:00
2021-08-12 14:10:45 -04:00
scope :without_hidden , - > {
if Feature . enabled? ( :ban_user_feature_flag )
2021-09-16 17:11:39 -04:00
where ( 'NOT EXISTS (?)' , Users :: BannedUser . select ( 1 ) . where ( 'issues.author_id = banned_users.user_id' ) )
2021-08-12 14:10:45 -04:00
else
all
end
}
2019-11-19 07:06:00 -05:00
scope :counts_by_state , - > { reorder ( nil ) . group ( :state_id ) . count }
2020-07-09 05:09:27 -04:00
scope :service_desk , - > { where ( author : :: User . support_bot ) }
2021-01-28 13:09:27 -05:00
scope :inc_relations_for_view , - > { includes ( author : :status , assignees : :status ) }
2020-07-09 05:09:27 -04:00
2020-04-09 20:10:04 -04:00
# An issue can be uniquely identified by project_id and iid
# Takes one or more sets of composite IDs, expressed as hash-like records of
# `{project_id: x, iid: y}`.
#
# @see WhereComposite::where_composite
#
# e.g:
#
# .by_project_id_and_iid({project_id: 1, iid: 2})
# .by_project_id_and_iid([]) # returns ActiveRecord::NullRelation
# .by_project_id_and_iid([
# {project_id: 1, iid: 1},
# {project_id: 2, iid: 1},
# {project_id: 1, iid: 2}
# ])
#
scope :by_project_id_and_iid , - > ( composites ) do
where_composite ( % i [ project_id iid ] , composites )
end
2021-10-13 11:12:51 -04:00
scope :with_null_relative_position , - > { where ( relative_position : nil ) }
scope :with_non_null_relative_position , - > { where . not ( relative_position : nil ) }
2020-04-09 20:10:04 -04:00
2020-01-14 10:07:55 -05:00
after_commit :expire_etag_cache , unless : :importing?
after_save :ensure_metrics , unless : :importing?
2020-09-28 08:10:02 -04:00
after_create_commit :record_create_action , unless : :importing?
2017-04-05 21:13:06 -04:00
2016-08-05 18:10:08 -04:00
attr_spammable :title , spam_title : true
attr_spammable :description , spam_description : true
2016-07-28 20:02:56 -04:00
2019-11-26 10:06:50 -05:00
state_machine :state_id , initial : :opened , initialize : false do
2013-02-18 04:10:58 -05:00
event :close do
2017-07-19 10:03:50 -04:00
transition [ :opened ] = > :closed
2013-02-18 04:10:58 -05:00
end
event :reopen do
2017-07-19 10:03:50 -04:00
transition closed : :opened
2013-02-18 04:10:58 -05:00
end
2019-10-18 07:11:44 -04:00
state :opened , value : Issue . available_states [ :opened ]
state :closed , value : Issue . available_states [ :closed ]
2017-03-15 16:58:09 -04:00
2021-05-11 02:10:29 -04:00
before_transition any = > :closed do | issue , transition |
args = transition . args
2019-04-08 11:33:30 -04:00
issue . closed_at = issue . system_note_timestamp
2021-05-11 02:10:29 -04:00
next if args . empty?
next unless args . first . is_a? ( User )
issue . closed_by = args . first
2017-03-15 16:58:09 -04:00
end
2018-03-05 09:27:53 -05:00
before_transition closed : :opened do | issue |
issue . closed_at = nil
issue . closed_by = nil
2021-11-09 10:12:42 -05:00
issue . clear_closure_reason_references
2018-03-05 09:27:53 -05:00
end
2013-02-18 04:10:58 -05:00
end
2013-04-09 08:04:31 -04:00
2019-10-18 07:11:44 -04:00
class << self
2021-07-14 23:09:42 -04:00
extend :: Gitlab :: Utils :: Override
# Alias to state machine .with_state_id method
# This needs to be defined after the state machine block to avoid errors
2019-10-18 07:11:44 -04:00
alias_method :with_state , :with_state_id
alias_method :with_states , :with_state_ids
2021-07-14 23:09:42 -04:00
override :order_upvotes_desc
def order_upvotes_desc
reorder ( upvotes_count : :desc )
end
override :order_upvotes_asc
def order_upvotes_asc
reorder ( upvotes_count : :asc )
end
2019-10-18 07:11:44 -04:00
end
2021-12-09 07:15:43 -05:00
def next_object_by_relative_position ( ignoring : nil , order : :asc )
return super unless Feature . enabled? ( :optimized_issue_neighbor_queries , project , default_enabled : :yaml )
array_mapping_scope = - > ( id_expression ) do
relation = Issue . where ( Issue . arel_table [ :project_id ] . eq ( id_expression ) )
if order == :asc
relation . where ( Issue . arel_table [ :relative_position ] . gt ( relative_position ) )
else
relation . where ( Issue . arel_table [ :relative_position ] . lt ( relative_position ) )
end
end
relation = Gitlab :: Pagination :: Keyset :: InOperatorOptimization :: QueryBuilder . new (
scope : Issue . order ( relative_position : order , id : order ) ,
array_scope : relative_positioning_parent_projects ,
array_mapping_scope : array_mapping_scope ,
finder_query : - > ( _ , id_expression ) { Issue . where ( Issue . arel_table [ :id ] . eq ( id_expression ) ) }
) . execute
relation = exclude_self ( relation , excluded : ignoring ) if ignoring . present?
relation . take
end
def relative_positioning_parent_projects
project . group & . root_ancestor & . all_projects & . select ( :id ) || Project . id_in ( project ) . select ( :id )
end
2019-07-22 03:47:29 -04:00
def self . relative_positioning_query_base ( issue )
2021-12-09 07:15:43 -05:00
in_projects ( issue . relative_positioning_parent_projects )
2018-01-04 07:29:48 -05:00
end
2019-07-22 03:47:29 -04:00
def self . relative_positioning_parent_column
2018-11-23 08:06:04 -05:00
:project_id
end
2015-05-02 23:11:21 -04:00
def self . reference_prefix
'#'
end
2015-05-14 16:59:39 -04:00
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
def self . reference_pattern
2016-03-24 11:41:48 -04:00
@reference_pattern || = %r{
2015-12-01 09:51:27 -05:00
( #{Project.reference_pattern})?
2020-02-18 07:09:15 -05:00
#{Regexp.escape(reference_prefix)}#{Gitlab::Regex.issue}
2015-05-14 16:59:39 -04:00
} x
2014-09-15 03:10:35 -04:00
end
2015-11-30 15:14:46 -05:00
def self . link_reference_pattern
2020-02-18 07:09:15 -05:00
@link_reference_pattern || = super ( " issues " , Gitlab :: Regex . issue )
2015-11-30 15:14:46 -05:00
end
2016-06-18 13:55:45 -04:00
def self . reference_valid? ( reference )
reference . to_i > 0 && reference . to_i < = Gitlab :: Database :: MAX_INT_VALUE
end
2016-10-19 09:49:07 -04:00
def self . project_foreign_key
'project_id'
end
2020-02-04 07:09:00 -05:00
def self . simple_sorts
super . merge (
{
'closest_future_date' = > - > { order_closest_future_date } ,
'closest_future_date_asc' = > - > { order_closest_future_date } ,
'due_date' = > - > { order_due_date_asc . with_order_id_desc } ,
'due_date_asc' = > - > { order_due_date_asc . with_order_id_desc } ,
'due_date_desc' = > - > { order_due_date_desc . with_order_id_desc } ,
2021-09-28 20:10:07 -04:00
'relative_position' = > - > { order_by_relative_position } ,
'relative_position_asc' = > - > { order_by_relative_position }
2020-02-04 07:09:00 -05:00
}
)
end
2018-04-04 05:19:47 -04:00
def self . sort_by_attribute ( method , excluded_labels : [ ] )
2016-04-19 07:10:25 -04:00
case method . to_s
2019-08-15 06:56:33 -04:00
when 'closest_future_date' , 'closest_future_date_asc' then order_closest_future_date
2019-11-12 22:06:31 -05:00
when 'due_date' , 'due_date_asc' then order_due_date_asc . with_order_id_desc
when 'due_date_desc' then order_due_date_desc . with_order_id_desc
2021-09-28 20:10:07 -04:00
when 'relative_position' , 'relative_position_asc' then order_by_relative_position
2020-09-25 05:09:40 -04:00
when 'severity_asc' then order_severity_asc . with_order_id_desc
when 'severity_desc' then order_severity_desc . with_order_id_desc
2016-04-19 07:10:25 -04:00
else
super
end
end
2021-09-28 20:10:07 -04:00
def self . order_by_relative_position
reorder ( Gitlab :: Pagination :: Keyset :: Order . build ( [ column_order_relative_position , column_order_id_asc ] ) )
2021-07-07 05:08:35 -04:00
end
def self . column_order_relative_position
Gitlab :: Pagination :: Keyset :: ColumnOrderDefinition . new (
attribute_name : 'relative_position' ,
column_expression : arel_table [ :relative_position ] ,
order_expression : Gitlab :: Database . nulls_last_order ( 'issues.relative_position' , 'ASC' ) ,
reversed_order_expression : Gitlab :: Database . nulls_last_order ( 'issues.relative_position' , 'DESC' ) ,
order_direction : :asc ,
nullable : :nulls_last ,
distinct : false
)
end
2021-09-15 08:11:13 -04:00
def self . column_order_id_asc
Gitlab :: Pagination :: Keyset :: ColumnOrderDefinition . new (
attribute_name : 'id' ,
order_expression : arel_table [ :id ] . asc
)
end
2021-07-23 08:09:05 -04:00
def self . to_branch_name ( * args )
branch_name = args . map ( & :to_s ) . each_with_index . map do | arg , i |
arg . parameterize ( preserve_case : i == 0 ) . presence
end . compact . join ( '-' )
if branch_name . length > 100
truncated_string = branch_name [ 0 , 100 ]
# Delete everything dangling after the last hyphen so as not to risk
# existence of unintended words in the branch name due to mid-word split.
branch_name = truncated_string . sub ( / -[^-]* \ Z / , '' )
end
branch_name
end
2021-05-17 14:10:42 -04:00
# Temporary disable moving null elements because of performance problems
# For more information check https://gitlab.com/gitlab-com/gl-infra/production/-/issues/4321
def check_repositioning_allowed!
if blocked_for_repositioning?
raise :: Gitlab :: RelativePositioning :: IssuePositioningDisabled , " Issue relative position changes temporarily disabled. "
end
end
def blocked_for_repositioning?
resource_parent . root_namespace & . issue_repositioning_disabled?
end
2017-09-19 13:23:15 -04:00
def hook_attrs
2017-10-05 13:02:50 -04:00
Gitlab :: HookData :: IssueBuilder . new ( self ) . build
2017-09-19 13:23:15 -04:00
end
2017-01-11 07:44:12 -05:00
# `from` argument can be a Namespace or Project.
2017-01-10 18:52:25 -05:00
def to_reference ( from = nil , full : false )
2015-05-02 23:11:21 -04:00
reference = " #{ self . class . reference_prefix } #{ iid } "
2020-01-29 07:09:08 -05:00
" #{ project . to_reference_base ( from , full : full ) } #{ reference } "
2015-05-02 23:11:21 -04:00
end
2018-04-05 11:17:02 -04:00
def suggested_branch_name
return to_branch_name unless project . repository . branch_exists? ( to_branch_name )
2018-04-19 02:59:37 -04:00
start_counting_from = 2
2018-04-19 05:31:01 -04:00
Uniquify . new ( start_counting_from ) . string ( - > ( counter ) { " #{ to_branch_name } - #{ counter } " } ) do | suggested_branch_name |
2018-04-19 02:59:37 -04:00
project . repository . branch_exists? ( suggested_branch_name )
end
2018-04-05 11:17:02 -04:00
end
2017-05-04 04:09:21 -04:00
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
2017-05-11 11:47:44 -04:00
def has_related_branch?
2017-05-04 04:09:21 -04:00
project . repository . branch_names . any? do | branch |
/ \ A #{ iid } -(?! \ d+-stable) /i =~ branch
end
end
2014-09-20 10:06:35 -04:00
# To allow polymorphism with MergeRequest.
def source_project
project
end
2015-10-12 06:04:20 -04:00
2016-03-17 06:11:22 -04:00
def moved?
2019-08-30 02:16:01 -04:00
! moved_to_id . nil?
2016-03-17 06:11:22 -04:00
end
2019-09-13 09:26:31 -04:00
def duplicated?
! duplicated_to_id . nil?
end
2021-11-09 10:12:42 -05:00
def clear_closure_reason_references
self . moved_to_id = nil
self . duplicated_to_id = nil
end
2016-03-17 06:11:22 -04:00
def can_move? ( user , to_project = nil )
if to_project
return false unless user . can? ( :admin_issue , to_project )
end
2016-03-23 04:39:37 -04:00
! moved? && persisted? &&
user . can? ( :admin_issue , self . project )
2016-03-17 06:11:22 -04:00
end
2020-12-03 16:09:35 -05:00
alias_method :can_clone? , :can_move?
2016-03-19 13:50:15 -04:00
2016-02-12 13:42:25 -05:00
def to_branch_name
2016-04-12 00:29:01 -04:00
if self . confidential?
2016-04-18 23:52:55 -04:00
" #{ iid } -confidential-issue "
2016-04-12 00:29:01 -04:00
else
2021-07-23 08:09:05 -04:00
self . class . to_branch_name ( iid , title )
2016-04-12 00:29:01 -04:00
end
2016-02-12 13:42:25 -05:00
end
2020-08-20 20:10:44 -04:00
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' ,
2020-11-19 19:09:06 -05:00
'issue_links.target_id as issue_link_source_id' ,
'issue_links.created_at as issue_link_created_at' ,
'issue_links.updated_at as issue_link_updated_at' ] )
2020-08-20 20:10:44 -04:00
. 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' )
2021-04-13 11:11:24 -04:00
related_issues = yield related_issues if block_given?
2020-08-20 20:10:44 -04:00
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
2018-04-05 11:17:02 -04:00
def can_be_worked_on?
! self . closed? && ! self . project . forked?
2016-02-12 13:42:25 -05:00
end
2016-03-18 13:59:57 -04:00
2016-07-20 14:13:02 -04:00
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user? ( user = nil )
2019-04-09 11:38:58 -04:00
return publicly_visible? unless user
return false unless readable_by? ( user )
2019-12-17 04:07:48 -05:00
user . can_read_all_resources? ||
2019-04-09 11:38:58 -04:00
:: Gitlab :: ExternalAuthorization . access_allowed? (
user , project . external_authorization_classification_label )
2016-07-20 14:13:02 -04:00
end
2021-07-20 02:08:37 -04:00
def check_for_spam? ( user : )
2021-07-09 08:08:17 -04:00
# content created via support bots is always checked for spam, EVEN if
# the issue is not publicly visible and/or confidential
2021-07-20 02:08:37 -04:00
return true if user . support_bot? && spammable_attribute_changed?
2021-07-09 08:08:17 -04:00
# Only check for spam on issues which are publicly visible (and thus indexed in search engines)
return false unless publicly_visible?
# Only check for spam if certain attributes have changed
spammable_attribute_changed?
2016-07-30 00:18:32 -04:00
end
2016-10-07 04:24:57 -04:00
def as_json ( options = { } )
super ( options ) . tap do | json |
2017-06-02 13:11:26 -04:00
if options . key? ( :labels )
2016-10-14 19:51:41 -04:00
json [ :labels ] = labels . as_json (
project : project ,
2016-10-19 17:33:34 -04:00
only : [ :id , :title , :description , :color , :priority ] ,
2016-10-14 19:51:41 -04:00
methods : [ :text_color ]
)
end
2016-10-07 04:24:57 -04:00
end
end
2016-11-01 16:18:51 -04:00
2018-06-21 08:22:40 -04:00
def etag_caching_enabled?
true
end
2017-08-17 13:26:45 -04:00
def discussions_rendered_on_frontend?
true
end
2018-08-27 11:31:01 -04:00
# rubocop: disable CodeReuse/ServiceClass
2017-08-17 11:21:25 -04:00
def update_project_counter_caches
Projects :: OpenIssuesCountService . new ( project ) . refresh_cache
end
2018-08-27 11:31:01 -04:00
# rubocop: enable CodeReuse/ServiceClass
2017-08-17 11:21:25 -04:00
2019-06-12 12:28:25 -04:00
def merge_requests_count ( user = nil )
:: MergeRequestsClosingIssues . count_for_issue ( self . id , user )
2019-02-27 05:28:53 -05:00
end
2019-06-24 05:51:34 -04:00
def labels_hook_attrs
labels . map ( & :hook_attrs )
end
2020-03-04 04:08:20 -05:00
def previous_updated_at
previous_changes [ 'updated_at' ] & . first || updated_at
end
2020-05-27 02:08:13 -04:00
def banzai_render_context ( field )
super . merge ( label_url_method : :project_issues_url )
end
2020-05-04 02:10:10 -04:00
def design_collection
@design_collection || = :: DesignManagement :: DesignCollection . new ( self )
end
2020-07-09 05:09:27 -04:00
def from_service_desk?
author . id == User . support_bot . id
end
2020-08-20 20:10:44 -04:00
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
2020-09-25 11:09:36 -04:00
def relocation_target
moved_to || duplicated_to
end
2021-01-05 07:10:36 -05:00
def supports_assignee?
issue_type_supports? ( :assignee )
end
2021-06-02 05:09:46 -04:00
def supports_time_tracking?
issue_type_supports? ( :time_tracking )
end
2021-09-27 08:13:56 -04:00
def supports_move_and_clone?
issue_type_supports? ( :move_and_clone )
end
2021-03-15 11:09:07 -04:00
def email_participants_emails
issue_email_participants . pluck ( :email )
end
def email_participants_emails_downcase
2021-02-22 07:10:38 -05:00
issue_email_participants . pluck ( IssueEmailParticipant . arel_table [ :email ] . lower )
end
2021-03-31 05:09:12 -04:00
def issue_assignee_user_ids
issue_assignees . pluck ( :user_id )
end
2021-07-14 08:09:23 -04:00
def update_upvotes_count
self . lock!
self . update_column ( :upvotes_count , self . upvotes )
end
2021-08-03 17:09:39 -04:00
# Returns `true` if the given User can read the current Issue.
#
# This method duplicates the same check of issue_policy.rb
# for performance reasons, check commit: 002ad215818450d2cbbc5fa065850a953dc7ada8
# Make sure to sync this method with issue_policy.rb
def readable_by? ( user )
if user . can_read_all_resources?
true
elsif project . owner == user
true
elsif confidential? && ! assignee_or_author? ( user )
project . team . member? ( user , Gitlab :: Access :: REPORTER )
2021-08-12 14:10:45 -04:00
elsif hidden?
false
2021-12-03 13:11:11 -05:00
elsif project . public? || ( project . internal? && ! user . external? )
project . feature_available? ( :issues , user )
2021-08-03 17:09:39 -04:00
else
2021-12-03 13:11:11 -05:00
project . team . member? ( user )
2021-08-03 17:09:39 -04:00
end
end
2021-08-12 14:10:45 -04:00
def hidden?
author & . banned?
end
2021-12-24 13:13:33 -05:00
# Necessary until all issues are backfilled and we add a NOT NULL constraint on the DB
def work_item_type
super || WorkItems :: Type . default_by_type ( issue_type )
end
2016-11-01 16:18:51 -04:00
private
2021-07-09 08:08:17 -04:00
def spammable_attribute_changed?
title_changed? ||
description_changed? ||
# NOTE: We need to check them for spam when issues are made non-confidential, because spam
# may have been added while they were confidential and thus not being checked for spam.
confidential_changed? ( from : true , to : false )
end
2021-03-24 08:09:32 -04:00
override :ensure_metrics
2017-12-07 12:41:30 -05:00
def ensure_metrics
2021-08-25 08:11:32 -04:00
Issue :: Metrics . record! ( self )
2017-12-07 12:41:30 -05:00
end
2020-09-28 08:10:02 -04:00
def record_create_action
Gitlab :: UsageDataCounters :: IssueActivityUniqueCounter . track_issue_created_action ( author : author )
end
2016-11-01 16:18:51 -04:00
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
2021-08-12 14:10:45 -04:00
project . public? && ! confidential? && ! hidden? && ! :: Gitlab :: ExternalAuthorization . enabled?
2016-11-01 16:18:51 -04:00
end
2017-04-05 21:13:06 -04:00
def expire_etag_cache
2017-06-29 13:06:35 -04:00
key = Gitlab :: Routing . url_helpers . realtime_changes_project_issue_path ( project , self )
2017-04-05 21:13:06 -04:00
Gitlab :: EtagCaching :: Store . new . touch ( key )
end
2020-08-27 14:10:29 -04:00
2020-09-16 14:09:47 -04:00
def could_not_move ( exception )
2020-08-27 14:10:29 -04:00
# Symptom of running out of space - schedule rebalancing
2021-12-02 16:10:16 -05:00
Issues :: RebalancingWorker . perform_async ( nil , * project . self_or_root_group_ids )
2020-08-27 14:10:29 -04:00
end
2011-10-08 17:36:38 -04:00
end
2019-09-13 09:26:31 -04:00
2021-05-11 17:10:21 -04:00
Issue . prepend_mod_with ( 'Issue' )