6ec53f5d48
Every project page displays a navigation menu that in turn displays the number of open issues and merge requests. This means that for every project page we run two COUNT(*) queries, each taking up roughly 30 milliseconds on GitLab.com. By caching these numbers and refreshing them whenever necessary we can reduce loading times of all these pages by up to roughly 60 milliseconds. The number of open issues does not include confidential issues. This is a trade-off to keep the code simple and to ensure refreshing the data only needs 2 COUNT(*) queries instead of 3. A downside is that if a project only has 5 confidential issues the counter will be set to 0. Because we now have 3 similar counting service classes the code previously used in Projects::ForksCountService has mostly been moved to Projects::CountService, which in turn is reused by the various service classes. Fixes https://gitlab.com/gitlab-org/gitlab-ce/issues/36622
357 lines
10 KiB
Ruby
357 lines
10 KiB
Ruby
class IssuableBaseService < BaseService
|
|
private
|
|
|
|
def create_milestone_note(issuable)
|
|
SystemNoteService.change_milestone(
|
|
issuable, issuable.project, current_user, issuable.milestone)
|
|
end
|
|
|
|
def create_labels_note(issuable, old_labels)
|
|
added_labels = issuable.labels - old_labels
|
|
removed_labels = old_labels - issuable.labels
|
|
|
|
SystemNoteService.change_label(
|
|
issuable, issuable.project, current_user, added_labels, removed_labels)
|
|
end
|
|
|
|
def create_title_change_note(issuable, old_title)
|
|
SystemNoteService.change_title(
|
|
issuable, issuable.project, current_user, old_title)
|
|
end
|
|
|
|
def create_description_change_note(issuable)
|
|
SystemNoteService.change_description(issuable, issuable.project, current_user)
|
|
end
|
|
|
|
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
|
|
SystemNoteService.change_branch(
|
|
issuable, issuable.project, current_user, branch_type,
|
|
old_branch, new_branch)
|
|
end
|
|
|
|
def create_task_status_note(issuable)
|
|
issuable.updated_tasks.each do |task|
|
|
SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
|
|
end
|
|
end
|
|
|
|
def create_time_estimate_note(issuable)
|
|
SystemNoteService.change_time_estimate(issuable, issuable.project, current_user)
|
|
end
|
|
|
|
def create_time_spent_note(issuable)
|
|
SystemNoteService.change_time_spent(issuable, issuable.project, current_user)
|
|
end
|
|
|
|
def filter_params(issuable)
|
|
ability_name = :"admin_#{issuable.to_ability_name}"
|
|
|
|
unless can?(current_user, ability_name, project)
|
|
params.delete(:milestone_id)
|
|
params.delete(:labels)
|
|
params.delete(:add_label_ids)
|
|
params.delete(:remove_label_ids)
|
|
params.delete(:label_ids)
|
|
params.delete(:assignee_ids)
|
|
params.delete(:assignee_id)
|
|
params.delete(:due_date)
|
|
params.delete(:canonical_issue_id)
|
|
end
|
|
|
|
filter_assignee(issuable)
|
|
filter_milestone
|
|
filter_labels
|
|
end
|
|
|
|
def filter_assignee(issuable)
|
|
return unless params[:assignee_id].present?
|
|
|
|
assignee_id = params[:assignee_id]
|
|
|
|
if assignee_id.to_s == IssuableFinder::NONE
|
|
params[:assignee_id] = ""
|
|
else
|
|
params.delete(:assignee_id) unless assignee_can_read?(issuable, assignee_id)
|
|
end
|
|
end
|
|
|
|
def assignee_can_read?(issuable, assignee_id)
|
|
new_assignee = User.find_by_id(assignee_id)
|
|
|
|
return false unless new_assignee
|
|
|
|
ability_name = :"read_#{issuable.to_ability_name}"
|
|
resource = issuable.persisted? ? issuable : project
|
|
|
|
can?(new_assignee, ability_name, resource)
|
|
end
|
|
|
|
def filter_milestone
|
|
milestone_id = params[:milestone_id]
|
|
return unless milestone_id
|
|
|
|
params[:milestone_id] = '' if milestone_id == IssuableFinder::NONE
|
|
|
|
milestone =
|
|
Milestone.for_projects_and_groups([project.id], [project.group&.id]).find_by_id(milestone_id)
|
|
|
|
params[:milestone_id] = '' unless milestone
|
|
end
|
|
|
|
def filter_labels
|
|
filter_labels_in_param(:add_label_ids)
|
|
filter_labels_in_param(:remove_label_ids)
|
|
filter_labels_in_param(:label_ids)
|
|
find_or_create_label_ids
|
|
end
|
|
|
|
def filter_labels_in_param(key)
|
|
return if params[key].to_a.empty?
|
|
|
|
params[key] = available_labels.where(id: params[key]).pluck(:id)
|
|
end
|
|
|
|
def find_or_create_label_ids
|
|
labels = params.delete(:labels)
|
|
|
|
return unless labels
|
|
|
|
params[:label_ids] = labels.split(",").map do |label_name|
|
|
service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
|
|
label = service.execute
|
|
|
|
label.try(:id)
|
|
end.compact
|
|
end
|
|
|
|
def process_label_ids(attributes, existing_label_ids: nil)
|
|
label_ids = attributes.delete(:label_ids)
|
|
add_label_ids = attributes.delete(:add_label_ids)
|
|
remove_label_ids = attributes.delete(:remove_label_ids)
|
|
|
|
new_label_ids = existing_label_ids || label_ids || []
|
|
|
|
if add_label_ids.blank? && remove_label_ids.blank?
|
|
new_label_ids = label_ids if label_ids
|
|
else
|
|
new_label_ids |= add_label_ids if add_label_ids
|
|
new_label_ids -= remove_label_ids if remove_label_ids
|
|
end
|
|
|
|
new_label_ids
|
|
end
|
|
|
|
def available_labels
|
|
LabelsFinder.new(current_user, project_id: @project.id).execute
|
|
end
|
|
|
|
def merge_quick_actions_into_params!(issuable)
|
|
description, command_params =
|
|
QuickActions::InterpretService.new(project, current_user)
|
|
.execute(params[:description], issuable)
|
|
|
|
# Avoid a description already set on an issuable to be overwritten by a nil
|
|
params[:description] = description if params.key?(:description)
|
|
|
|
params.merge!(command_params)
|
|
end
|
|
|
|
def create_issuable(issuable, attributes, label_ids:)
|
|
issuable.with_transaction_returning_status do
|
|
if issuable.save
|
|
issuable.update_attributes(label_ids: label_ids)
|
|
end
|
|
end
|
|
end
|
|
|
|
def create(issuable)
|
|
merge_quick_actions_into_params!(issuable)
|
|
filter_params(issuable)
|
|
|
|
params.delete(:state_event)
|
|
params[:author] ||= current_user
|
|
|
|
label_ids = process_label_ids(params)
|
|
|
|
issuable.assign_attributes(params)
|
|
|
|
before_create(issuable)
|
|
|
|
if params.present? && create_issuable(issuable, params, label_ids: label_ids)
|
|
after_create(issuable)
|
|
execute_hooks(issuable)
|
|
invalidate_cache_counts(issuable, users: issuable.assignees)
|
|
end
|
|
|
|
issuable
|
|
end
|
|
|
|
def before_create(issuable)
|
|
# To be overridden by subclasses
|
|
end
|
|
|
|
def after_create(issuable)
|
|
# To be overridden by subclasses
|
|
|
|
issuable.update_project_counter_caches
|
|
end
|
|
|
|
def before_update(issuable)
|
|
# To be overridden by subclasses
|
|
end
|
|
|
|
def after_update(issuable)
|
|
# To be overridden by subclasses
|
|
|
|
issuable.update_project_counter_caches
|
|
end
|
|
|
|
def update(issuable)
|
|
change_state(issuable)
|
|
change_subscription(issuable)
|
|
change_todo(issuable)
|
|
toggle_award(issuable)
|
|
filter_params(issuable)
|
|
old_labels = issuable.labels.to_a
|
|
old_mentioned_users = issuable.mentioned_users.to_a
|
|
old_assignees = issuable.assignees.to_a
|
|
|
|
label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
|
|
params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
|
|
|
|
if issuable.changed? || params.present?
|
|
issuable.assign_attributes(params.merge(updated_by: current_user))
|
|
|
|
if has_title_or_description_changed?(issuable)
|
|
issuable.assign_attributes(last_edited_at: Time.now, last_edited_by: current_user)
|
|
end
|
|
|
|
before_update(issuable)
|
|
|
|
if issuable.with_transaction_returning_status { issuable.save }
|
|
# We do not touch as it will affect a update on updated_at field
|
|
ActiveRecord::Base.no_touching do
|
|
handle_common_system_notes(issuable, old_labels: old_labels)
|
|
end
|
|
|
|
handle_changes(
|
|
issuable,
|
|
old_labels: old_labels,
|
|
old_mentioned_users: old_mentioned_users,
|
|
old_assignees: old_assignees
|
|
)
|
|
|
|
new_assignees = issuable.assignees.to_a
|
|
affected_assignees = (old_assignees + new_assignees) - (old_assignees & new_assignees)
|
|
|
|
# Don't clear the project cache, because it will be handled by the
|
|
# appropriate service (close / reopen / merge / etc.).
|
|
invalidate_cache_counts(issuable, users: affected_assignees.compact, skip_project_cache: true)
|
|
after_update(issuable)
|
|
issuable.create_new_cross_references!(current_user)
|
|
execute_hooks(issuable, 'update')
|
|
end
|
|
end
|
|
|
|
issuable
|
|
end
|
|
|
|
def labels_changing?(old_label_ids, new_label_ids)
|
|
old_label_ids.sort != new_label_ids.sort
|
|
end
|
|
|
|
def has_title_or_description_changed?(issuable)
|
|
issuable.title_changed? || issuable.description_changed?
|
|
end
|
|
|
|
def change_state(issuable)
|
|
case params.delete(:state_event)
|
|
when 'reopen'
|
|
reopen_service.new(project, current_user, {}).execute(issuable)
|
|
when 'close'
|
|
close_service.new(project, current_user, {}).execute(issuable)
|
|
end
|
|
end
|
|
|
|
def change_subscription(issuable)
|
|
case params.delete(:subscription_event)
|
|
when 'subscribe'
|
|
issuable.subscribe(current_user, project)
|
|
when 'unsubscribe'
|
|
issuable.unsubscribe(current_user, project)
|
|
end
|
|
end
|
|
|
|
def change_todo(issuable)
|
|
case params.delete(:todo_event)
|
|
when 'add'
|
|
todo_service.mark_todo(issuable, current_user)
|
|
when 'done'
|
|
todo = TodosFinder.new(current_user).execute.find_by(target: issuable)
|
|
todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo
|
|
end
|
|
end
|
|
|
|
def toggle_award(issuable)
|
|
award = params.delete(:emoji_award)
|
|
if award
|
|
todo_service.new_award_emoji(issuable, current_user)
|
|
issuable.toggle_award_emoji(award, current_user)
|
|
end
|
|
end
|
|
|
|
def has_changes?(issuable, old_labels: [], old_assignees: [])
|
|
valid_attrs = [:title, :description, :assignee_id, :milestone_id, :target_branch]
|
|
|
|
attrs_changed = valid_attrs.any? do |attr|
|
|
issuable.previous_changes.include?(attr.to_s)
|
|
end
|
|
|
|
labels_changed = issuable.labels != old_labels
|
|
|
|
assignees_changed = issuable.assignees != old_assignees
|
|
|
|
attrs_changed || labels_changed || assignees_changed
|
|
end
|
|
|
|
def handle_common_system_notes(issuable, old_labels: [])
|
|
if issuable.previous_changes.include?('title')
|
|
create_title_change_note(issuable, issuable.previous_changes['title'].first)
|
|
end
|
|
|
|
if issuable.previous_changes.include?('description')
|
|
if issuable.tasks? && issuable.updated_tasks.any?
|
|
create_task_status_note(issuable)
|
|
else
|
|
# TODO: Show this note if non-task content was modified.
|
|
# https://gitlab.com/gitlab-org/gitlab-ce/issues/33577
|
|
create_description_change_note(issuable)
|
|
end
|
|
end
|
|
|
|
if issuable.previous_changes.include?('time_estimate')
|
|
create_time_estimate_note(issuable)
|
|
end
|
|
|
|
if issuable.time_spent?
|
|
create_time_spent_note(issuable)
|
|
end
|
|
|
|
create_labels_note(issuable, old_labels) if issuable.labels != old_labels
|
|
end
|
|
|
|
def invalidate_cache_counts(issuable, users: [], skip_project_cache: false)
|
|
users.each do |user|
|
|
user.public_send("invalidate_#{issuable.model_name.singular}_cache_counts") # rubocop:disable GitlabSecurity/PublicSend
|
|
end
|
|
|
|
unless skip_project_cache
|
|
case issuable
|
|
when Issue
|
|
IssuesFinder.new(nil, project_id: issuable.project_id).clear_caches!
|
|
when MergeRequest
|
|
MergeRequestsFinder.new(nil, project_id: issuable.target_project_id).clear_caches!
|
|
end
|
|
end
|
|
end
|
|
end
|