c52a36e859
When displaying events we would load the target of those events, then render the entire data using our Markdown pipeline. This pipeline would eventually request the author of every target, leading to an additional query being executed for every target to get the author. To fix this we now eager load the author of the event's target. In my local environment this reduces the number of queries to display a project's Atom feed from 40 to 24 queries. See https://gitlab.com/gitlab-org/gitlab-ce/issues/36878 for more information.
385 lines
8.1 KiB
Ruby
385 lines
8.1 KiB
Ruby
class Event < ActiveRecord::Base
|
|
include Sortable
|
|
include IgnorableColumn
|
|
default_scope { reorder(nil) }
|
|
|
|
CREATED = 1
|
|
UPDATED = 2
|
|
CLOSED = 3
|
|
REOPENED = 4
|
|
PUSHED = 5
|
|
COMMENTED = 6
|
|
MERGED = 7
|
|
JOINED = 8 # User joined project
|
|
LEFT = 9 # User left project
|
|
DESTROYED = 10
|
|
EXPIRED = 11 # User left project due to expiry
|
|
|
|
ACTIONS = HashWithIndifferentAccess.new(
|
|
created: CREATED,
|
|
updated: UPDATED,
|
|
closed: CLOSED,
|
|
reopened: REOPENED,
|
|
pushed: PUSHED,
|
|
commented: COMMENTED,
|
|
merged: MERGED,
|
|
joined: JOINED,
|
|
left: LEFT,
|
|
destroyed: DESTROYED,
|
|
expired: EXPIRED
|
|
).freeze
|
|
|
|
TARGET_TYPES = HashWithIndifferentAccess.new(
|
|
issue: Issue,
|
|
milestone: Milestone,
|
|
merge_request: MergeRequest,
|
|
note: Note,
|
|
project: Project,
|
|
snippet: Snippet,
|
|
user: User
|
|
).freeze
|
|
|
|
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
|
|
|
|
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
|
|
delegate :title, to: :issue, prefix: true, allow_nil: true
|
|
delegate :title, to: :merge_request, prefix: true, allow_nil: true
|
|
delegate :title, to: :note, prefix: true, allow_nil: true
|
|
|
|
belongs_to :author, class_name: "User"
|
|
belongs_to :project
|
|
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
|
|
has_one :push_event_payload
|
|
|
|
# Callbacks
|
|
after_create :reset_project_activity
|
|
after_create :set_last_repository_updated_at, if: :push?
|
|
|
|
# Scopes
|
|
scope :recent, -> { reorder(id: :desc) }
|
|
scope :code_push, -> { where(action: PUSHED) }
|
|
|
|
scope :in_projects, -> (projects) do
|
|
sub_query = projects
|
|
.except(:order)
|
|
.select(1)
|
|
.where('projects.id = events.project_id')
|
|
|
|
where('EXISTS (?)', sub_query).recent
|
|
end
|
|
|
|
scope :with_associations, -> do
|
|
# We're using preload for "push_event_payload" as otherwise the association
|
|
# is not always available (depending on the query being built).
|
|
includes(:author, :project, project: :namespace)
|
|
.preload(:push_event_payload, target: :author)
|
|
end
|
|
|
|
scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
|
|
|
|
# Authors are required as they're used to display who pushed data.
|
|
#
|
|
# We're just validating the presence of the ID here as foreign key constraints
|
|
# should ensure the ID points to a valid user.
|
|
validates :author_id, presence: true
|
|
|
|
self.inheritance_column = 'action'
|
|
|
|
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
|
|
# include this column, hence we're ignoring it as well.
|
|
ignore_column :data
|
|
|
|
class << self
|
|
def model_name
|
|
ActiveModel::Name.new(self, nil, 'event')
|
|
end
|
|
|
|
def find_sti_class(action)
|
|
if action.to_i == PUSHED
|
|
PushEvent
|
|
else
|
|
Event
|
|
end
|
|
end
|
|
|
|
def subclass_from_attributes(attrs)
|
|
# Without this Rails will keep calling this method on the returned class,
|
|
# resulting in an infinite loop.
|
|
return unless self == Event
|
|
|
|
action = attrs.with_indifferent_access[inheritance_column].to_i
|
|
|
|
PushEvent if action == PUSHED
|
|
end
|
|
|
|
# Update Gitlab::ContributionsCalendar#activity_dates if this changes
|
|
def contributions
|
|
where("action = ? OR (target_type IN (?) AND action IN (?)) OR (target_type = ? AND action = ?)",
|
|
Event::PUSHED,
|
|
%w(MergeRequest Issue), [Event::CREATED, Event::CLOSED, Event::MERGED],
|
|
"Note", Event::COMMENTED)
|
|
end
|
|
|
|
def limit_recent(limit = 20, offset = nil)
|
|
recent.limit(limit).offset(offset)
|
|
end
|
|
|
|
def actions
|
|
ACTIONS.keys
|
|
end
|
|
|
|
def target_types
|
|
TARGET_TYPES.keys
|
|
end
|
|
end
|
|
|
|
def visible_to_user?(user = nil)
|
|
if push? || commit_note?
|
|
Ability.allowed?(user, :download_code, project)
|
|
elsif membership_changed?
|
|
true
|
|
elsif created_project?
|
|
true
|
|
elsif issue? || issue_note?
|
|
Ability.allowed?(user, :read_issue, note? ? note_target : target)
|
|
elsif merge_request? || merge_request_note?
|
|
Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
|
|
else
|
|
milestone?
|
|
end
|
|
end
|
|
|
|
def project_name
|
|
if project
|
|
project.name_with_namespace
|
|
else
|
|
"(deleted project)"
|
|
end
|
|
end
|
|
|
|
def target_title
|
|
target.try(:title)
|
|
end
|
|
|
|
def created?
|
|
action == CREATED
|
|
end
|
|
|
|
def push?
|
|
false
|
|
end
|
|
|
|
def merged?
|
|
action == MERGED
|
|
end
|
|
|
|
def closed?
|
|
action == CLOSED
|
|
end
|
|
|
|
def reopened?
|
|
action == REOPENED
|
|
end
|
|
|
|
def joined?
|
|
action == JOINED
|
|
end
|
|
|
|
def left?
|
|
action == LEFT
|
|
end
|
|
|
|
def expired?
|
|
action == EXPIRED
|
|
end
|
|
|
|
def destroyed?
|
|
action == DESTROYED
|
|
end
|
|
|
|
def commented?
|
|
action == COMMENTED
|
|
end
|
|
|
|
def membership_changed?
|
|
joined? || left? || expired?
|
|
end
|
|
|
|
def created_project?
|
|
created? && !target && target_type.nil?
|
|
end
|
|
|
|
def created_target?
|
|
created? && target
|
|
end
|
|
|
|
def milestone?
|
|
target_type == "Milestone"
|
|
end
|
|
|
|
def note?
|
|
target.is_a?(Note)
|
|
end
|
|
|
|
def issue?
|
|
target_type == "Issue"
|
|
end
|
|
|
|
def merge_request?
|
|
target_type == "MergeRequest"
|
|
end
|
|
|
|
def milestone
|
|
target if milestone?
|
|
end
|
|
|
|
def issue
|
|
target if issue?
|
|
end
|
|
|
|
def merge_request
|
|
target if merge_request?
|
|
end
|
|
|
|
def note
|
|
target if note?
|
|
end
|
|
|
|
def action_name
|
|
if push?
|
|
push_action_name
|
|
elsif closed?
|
|
"closed"
|
|
elsif merged?
|
|
"accepted"
|
|
elsif joined?
|
|
'joined'
|
|
elsif left?
|
|
'left'
|
|
elsif expired?
|
|
'removed due to membership expiration from'
|
|
elsif destroyed?
|
|
'destroyed'
|
|
elsif commented?
|
|
"commented on"
|
|
elsif created_project?
|
|
created_project_action_name
|
|
else
|
|
"opened"
|
|
end
|
|
end
|
|
|
|
def target_iid
|
|
target.respond_to?(:iid) ? target.iid : target_id
|
|
end
|
|
|
|
def commit_note?
|
|
note? && target && target.for_commit?
|
|
end
|
|
|
|
def issue_note?
|
|
note? && target && target.for_issue?
|
|
end
|
|
|
|
def merge_request_note?
|
|
note? && target && target.for_merge_request?
|
|
end
|
|
|
|
def project_snippet_note?
|
|
note? && target && target.for_snippet?
|
|
end
|
|
|
|
def note_target
|
|
target.noteable
|
|
end
|
|
|
|
def note_target_id
|
|
if commit_note?
|
|
target.commit_id
|
|
else
|
|
target.noteable_id.to_s
|
|
end
|
|
end
|
|
|
|
def note_target_reference
|
|
return unless note_target
|
|
|
|
# Commit#to_reference returns the full SHA, but we want the short one here
|
|
if commit_note?
|
|
note_target.short_id
|
|
else
|
|
note_target.to_reference
|
|
end
|
|
end
|
|
|
|
def note_target_type
|
|
if target.noteable_type.present?
|
|
target.noteable_type.titleize
|
|
else
|
|
"Wall"
|
|
end.downcase
|
|
end
|
|
|
|
def body?
|
|
if push?
|
|
push_with_commits?
|
|
elsif note?
|
|
true
|
|
else
|
|
target.respond_to? :title
|
|
end
|
|
end
|
|
|
|
def reset_project_activity
|
|
return unless project
|
|
|
|
# Don't bother updating if we know the project was updated recently.
|
|
return if recent_update?
|
|
|
|
# At this point it's possible for multiple threads/processes to try to
|
|
# update the project. Only one query should actually perform the update,
|
|
# hence we add the extra WHERE clause for last_activity_at.
|
|
Project.unscoped.where(id: project_id)
|
|
.where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago)
|
|
.update_all(last_activity_at: created_at)
|
|
end
|
|
|
|
def authored_by?(user)
|
|
user ? author_id == user.id : false
|
|
end
|
|
|
|
def to_partial_path
|
|
# We are intentionally using `Event` rather than `self.class` so that
|
|
# subclasses also use the `Event` implementation.
|
|
Event._to_partial_path
|
|
end
|
|
|
|
private
|
|
|
|
def push_action_name
|
|
if new_ref?
|
|
"pushed new"
|
|
elsif rm_ref?
|
|
"deleted"
|
|
else
|
|
"pushed to"
|
|
end
|
|
end
|
|
|
|
def created_project_action_name
|
|
if project.external_import?
|
|
"imported"
|
|
else
|
|
"created"
|
|
end
|
|
end
|
|
|
|
def recent_update?
|
|
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
|
|
end
|
|
|
|
def set_last_repository_updated_at
|
|
Project.unscoped.where(id: project_id)
|
|
.update_all(last_repository_updated_at: created_at)
|
|
end
|
|
end
|