gitlab-org--gitlab-foss/app/models/issue.rb

459 lines
15 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
require 'carrierwave/orm/activerecord'
class Issue < ApplicationRecord
include AtomicInternalId
2018-05-11 07:52:48 +00:00
include IidRoutes
include Issuable
include Noteable
include Referable
include Spammable
include FasterCacheKeys
2017-02-01 18:41:01 +00:00
include RelativePositioning
2017-11-01 17:35:14 +00:00
include TimeTrackable
include ThrottledTouch
2018-08-01 08:58:49 +00:00
include LabelEventable
include IgnorableColumns
include MilestoneEventable
include WhereComposite
include StateEventable
include IdInOrdered
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
SORTING_PREFERENCE_FIELD = :issues_sort
# 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
Merge Request on forked projects The good: - You can do a merge request for a forked commit and it will merge properly (i.e. it does work). - Push events take into account merge requests on forked projects - Tests around merge_actions now present, spinach, and other rspec tests - Satellites now clean themselves up rather then recreate The questionable: - Events only know about target projects - Project's merge requests only hold on to MR's where they are the target - All operations performed in the satellite The bad: - Duplication between project's repositories and satellites (e.g. commits_between) (for reference: http://feedback.gitlab.com/forums/176466-general/suggestions/3456722-merge-requests-between-projects-repos) Fixes: Make test repos/satellites only create when needed -Spinach/Rspec now only initialize test directory, and setup stubs (things that are relatively cheap) -project_with_code, source_project_with_code, and target_project_with_code now create/destroy their repos individually -fixed remote removal -How to merge renders properly -Update emails to show project/branches -Edit MR doesn't set target branch -Fix some failures on editing/creating merge requests, added a test -Added back a test around merge request observer -Clean up project_transfer_spec, Remove duplicate enable/disable observers -Ensure satellite lock files are cleaned up, Attempted to add some testing around these as well -Signifant speed ups for tests -Update formatting ordering in notes_on_merge_requests -Remove wiki schema update Fixes for search/search results -Search results was using by_project for a list of projects, updated this to use in_projects -updated search results to reference the correct (target) project -udpated search results to print both sides of the merge request Change-Id: I19407990a0950945cc95d62089cbcc6262dab1a8
2013-04-25 14:15:33 +00:00
belongs_to :project
has_one :namespace, through: :project
belongs_to :duplicated_to, class_name: 'Issue'
2018-02-09 18:23:55 +00:00
belongs_to :closed_by, class_name: 'User'
belongs_to :iteration, foreign_key: 'sprint_id'
belongs_to :moved_to, class_name: 'Issue'
has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id
has_internal_id :iid, scope: :project, track_if: -> { !importing? }, init: ->(s) { s&.project&.issues&.maximum(:iid) }
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
2016-07-01 13:34:10 +00:00
has_many :merge_requests_closing_issues,
class_name: 'MergeRequestsClosingIssues',
dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
has_many :zoom_meetings
has_many :user_mentions, class_name: "IssueUserMention", dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :sent_notifications, as: :noteable
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
has_one :issuable_severity
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
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
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 08:59:36 +00:00
accepts_nested_attributes_for :sentry_issue
Merge Request on forked projects The good: - You can do a merge request for a forked commit and it will merge properly (i.e. it does work). - Push events take into account merge requests on forked projects - Tests around merge_actions now present, spinach, and other rspec tests - Satellites now clean themselves up rather then recreate The questionable: - Events only know about target projects - Project's merge requests only hold on to MR's where they are the target - All operations performed in the satellite The bad: - Duplication between project's repositories and satellites (e.g. commits_between) (for reference: http://feedback.gitlab.com/forums/176466-general/suggestions/3456722-merge-requests-between-projects-repos) Fixes: Make test repos/satellites only create when needed -Spinach/Rspec now only initialize test directory, and setup stubs (things that are relatively cheap) -project_with_code, source_project_with_code, and target_project_with_code now create/destroy their repos individually -fixed remote removal -How to merge renders properly -Update emails to show project/branches -Edit MR doesn't set target branch -Fix some failures on editing/creating merge requests, added a test -Added back a test around merge request observer -Clean up project_transfer_spec, Remove duplicate enable/disable observers -Ensure satellite lock files are cleaned up, Attempted to add some testing around these as well -Signifant speed ups for tests -Update formatting ordering in notes_on_merge_requests -Remove wiki schema update Fixes for search/search results -Search results was using by_project for a list of projects, updated this to use in_projects -updated search results to reference the correct (target) project -udpated search results to print both sides of the merge request Change-Id: I19407990a0950945cc95d62089cbcc6262dab1a8
2013-04-25 14:15:33 +00:00
validates :project, presence: true
validates :issue_type, presence: true
enum issue_type: {
issue: 0,
incident: 1,
test_case: 2 ## EE-only
}
Merge Request on forked projects The good: - You can do a merge request for a forked commit and it will merge properly (i.e. it does work). - Push events take into account merge requests on forked projects - Tests around merge_actions now present, spinach, and other rspec tests - Satellites now clean themselves up rather then recreate The questionable: - Events only know about target projects - Project's merge requests only hold on to MR's where they are the target - All operations performed in the satellite The bad: - Duplication between project's repositories and satellites (e.g. commits_between) (for reference: http://feedback.gitlab.com/forums/176466-general/suggestions/3456722-merge-requests-between-projects-repos) Fixes: Make test repos/satellites only create when needed -Spinach/Rspec now only initialize test directory, and setup stubs (things that are relatively cheap) -project_with_code, source_project_with_code, and target_project_with_code now create/destroy their repos individually -fixed remote removal -How to merge renders properly -Update emails to show project/branches -Edit MR doesn't set target branch -Fix some failures on editing/creating merge requests, added a test -Added back a test around merge request observer -Clean up project_transfer_spec, Remove duplicate enable/disable observers -Ensure satellite lock files are cleaned up, Attempted to add some testing around these as well -Signifant speed ups for tests -Update formatting ordering in notes_on_merge_requests -Remove wiki schema update Fixes for search/search results -Search results was using by_project for a list of projects, updated this to use in_projects -updated search results to reference the correct (target) project -udpated search results to print both sides of the merge request Change-Id: I19407990a0950945cc95d62089cbcc6262dab1a8
2013-04-25 14:15:33 +00:00
alias_attribute :parent_ids, :project_id
alias_method :issuing_parent, :project
2016-01-22 09:24:38 +00:00
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
scope :with_due_date, -> { where.not(due_date: nil) }
scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
scope :not_authored_by, ->(user) { where.not(author_id: user) }
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')) }
scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
scope :with_web_entity_associations, -> { preload(:author, :project) }
scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) }
scope :with_label_attributes, ->(label_attributes) { joins(:labels).where(labels: label_attributes) }
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) }
scope :with_api_entity_associations, -> {
preload(:timelogs, :closed_by, :assignees, :author, :notes, :labels,
milestone: { project: [:route, { namespace: :route }] },
project: [:route, { namespace: :route }])
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
scope :public_only, -> { where(confidential: false) }
scope :confidential_only, -> { where(confidential: true) }
scope :counts_by_state, -> { reorder(nil).group(:state_id).count }
scope :service_desk, -> { where(author: ::User.support_bot) }
# 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
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
2017-04-06 01:13:06 +00:00
attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true
state_machine :state_id, initial: :opened, initialize: false do
2013-02-18 09:10:58 +00:00
event :close do
transition [:opened] => :closed
2013-02-18 09:10:58 +00:00
end
event :reopen do
transition closed: :opened
2013-02-18 09:10:58 +00:00
end
state :opened, value: Issue.available_states[:opened]
state :closed, value: Issue.available_states[:closed]
2017-03-15 20:58:09 +00:00
before_transition any => :closed do |issue|
issue.closed_at = issue.system_note_timestamp
2017-03-15 20:58:09 +00:00
end
2018-03-05 14:27:53 +00:00
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
end
2013-02-18 09:10:58 +00:00
end
# Alias to state machine .with_state_id method
# This needs to be defined after the state machine block to avoid errors
class << self
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
end
def self.relative_positioning_query_base(issue)
in_projects(issue.parent_ids)
end
def self.relative_positioning_parent_column
:project_id
end
def self.reference_prefix
'#'
end
# Pattern used to extract `#123` issue references from text
#
# This pattern supports cross-project references.
def self.reference_pattern
@reference_pattern ||= %r{
(#{Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}#{Gitlab::Regex.issue}
}x
2014-09-15 07:10:35 +00:00
end
def self.link_reference_pattern
@link_reference_pattern ||= super("issues", Gitlab::Regex.issue)
end
def self.reference_valid?(reference)
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
end
def self.project_foreign_key
'project_id'
end
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 },
'relative_position' => -> { order_relative_position_asc.with_order_id_desc },
'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc }
}
)
end
def self.sort_by_attribute(method, excluded_labels: [])
case method.to_s
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
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
when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
else
super
end
end
# `with_cte` argument allows sorting when using CTE queries and prevents
# errors in postgres when using CTE search optimisation
def self.order_by_position_and_priority(with_cte: false)
order_labels_priority(with_cte: with_cte)
2017-06-21 13:48:12 +00:00
.reorder(Gitlab::Database.nulls_last_order('relative_position', 'ASC'),
Gitlab::Database.nulls_last_order('highest_priority', 'ASC'),
"id DESC")
end
def hook_attrs
Gitlab::HookData::IssueBuilder.new(self).build
end
# `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}"
"#{project.to_reference_base(from, full: full)}#{reference}"
end
def suggested_branch_name
return to_branch_name unless project.repository.branch_exists?(to_branch_name)
start_counting_from = 2
Uniquify.new(start_counting_from).string(-> (counter) { "#{to_branch_name}-#{counter}" }) do |suggested_branch_name|
project.repository.branch_exists?(suggested_branch_name)
end
end
# Returns boolean if a related branch exists for the current issue
# ignores merge requests branchs
def has_related_branch?
project.repository.branch_names.any? do |branch|
/\A#{iid}-(?!\d+-stable)/i =~ branch
end
end
# To allow polymorphism with MergeRequest.
def source_project
project
end
def moved?
!moved_to_id.nil?
end
def duplicated?
!duplicated_to_id.nil?
end
def can_move?(user, to_project = nil)
if to_project
return false unless user.can?(:admin_issue, to_project)
end
!moved? && persisted? &&
user.can?(:admin_issue, self.project)
end
2016-02-12 18:42:25 +00:00
def to_branch_name
if self.confidential?
"#{iid}-confidential-issue"
else
branch_name = "#{iid}-#{title.parameterize}"
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[0, truncated_string.rindex("-")]
end
branch_name
end
2016-02-12 18:42:25 +00:00
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?
!self.closed? && !self.project.forked?
2016-02-12 18:42:25 +00:00
end
# 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)
return false unless project && project.feature_available?(:issues, user)
return publicly_visible? unless user
return false unless readable_by?(user)
user.can_read_all_resources? ||
::Gitlab::ExternalAuthorization.access_allowed?(
user, project.external_authorization_classification_label)
end
def check_for_spam?
publicly_visible? &&
(title_changed? || description_changed? || confidential_changed?)
end
2016-10-07 08:24:57 +00:00
def as_json(options = {})
super(options).tap do |json|
if options.key?(:labels)
json[:labels] = labels.as_json(
project: project,
only: [:id, :title, :description, :color, :priority],
methods: [:text_color]
)
end
2016-10-07 08:24:57 +00:00
end
end
2018-06-21 12:22:40 +00:00
def etag_caching_enabled?
true
end
2017-08-17 17:26:45 +00:00
def discussions_rendered_on_frontend?
true
end
# rubocop: disable CodeReuse/ServiceClass
def update_project_counter_caches
Projects::OpenIssuesCountService.new(project).refresh_cache
end
# rubocop: enable CodeReuse/ServiceClass
def merge_requests_count(user = nil)
::MergeRequestsClosingIssues.count_for_issue(self.id, user)
end
def labels_hook_attrs
labels.map(&:hook_attrs)
end
def previous_updated_at
previous_changes['updated_at']&.first || updated_at
end
def banzai_render_context(field)
super.merge(label_url_method: :project_issues_url)
end
def design_collection
@design_collection ||= ::DesignManagement::DesignCollection.new(self)
end
def from_service_desk?
author.id == User.support_bot.id
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
def ensure_metrics
super
metrics.record!
end
# 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)
else
project.public? ||
project.internal? && !user.external? ||
project.team.member?(user)
end
end
# Returns `true` if this Issue is visible to everybody.
def publicly_visible?
project.public? && !confidential? && !::Gitlab::ExternalAuthorization.enabled?
end
2017-04-06 01:13:06 +00:00
def expire_etag_cache
key = Gitlab::Routing.url_helpers.realtime_changes_project_issue_path(project, self)
2017-04-06 01:13:06 +00:00
Gitlab::EtagCaching::Store.new.touch(key)
end
def could_not_move(exception)
# Symptom of running out of space - schedule rebalancing
IssueRebalancingWorker.perform_async(nil, project_id)
end
2011-10-08 21:36:38 +00:00
end
Issue.prepend_if_ee('EE::Issue')