# frozen_string_literal: true # A collection of Commit instances for a specific container and Git reference. class CommitCollection include Enumerable include Gitlab::Utils::StrongMemoize attr_reader :container, :ref, :commits delegate :repository, to: :container, allow_nil: true delegate :project, to: :repository, allow_nil: true # container - The object the commits belong to. # commits - The Commit instances to store. # ref - The name of the ref (e.g. "master"). def initialize(container, commits, ref = nil, page: nil, per_page: nil, count: nil) @container = container @commits = commits @ref = ref @pagination = Gitlab::PaginationDelegate.new(page: page, per_page: per_page, count: count) end def each(&block) commits.each(&block) end def committers(with_merge_commits: false) emails = if with_merge_commits commits.filter_map(&:committer_email).uniq else without_merge_commits.filter_map(&:committer_email).uniq end User.by_any_email(emails) end def committer_user_ids committers.pluck(:id) end def without_merge_commits strong_memoize(:without_merge_commits) do # `#enrich!` the collection to ensure all commits contain # the necessary parent data enrich!.commits.reject(&:merge_commit?) end end # Returns the collection with the latest pipeline for every commit pre-set. # # Setting the pipeline for each commit ahead of time removes the need for running # a query for every commit we're displaying. def with_latest_pipeline(ref = nil) return self unless project pipelines = project.ci_pipelines.latest_pipeline_per_commit(map(&:id), ref) each do |commit| pipeline = pipelines[commit.id] pipeline&.number_of_warnings # preload number of warnings commit.set_latest_pipeline_for_ref(ref, pipeline) end self end # Returns the collection with markdown fields preloaded. # # Get the markdown cache from redis using pipeline to prevent n+1 requests # when rendering the markdown of an attribute (e.g. title, full_title, # description). def with_markdown_cache Commit.preload_markdown_cache!(commits) self end def unenriched commits.reject(&:gitaly_commit?) end def fully_enriched? unenriched.empty? end # Batch load any commits that are not backed by full gitaly data, and # replace them in the collection. def enrich! # A container is needed in order to fetch data from gitaly. Containers # can be absent from commits in certain rare situations (like when # viewing a MR of a deleted fork). In these cases, assume that the # enriched data is not needed. return self if container.blank? || fully_enriched? # Batch load full Commits from the repository # and map to a Hash of id => Commit replacements = unenriched.each_with_object({}) do |c, result| result[c.id] = Commit.lazy(container, c.id) end.compact # Replace the commits, keeping the same order @commits = @commits.map do |original_commit| # Return the original instance: if it didn't need to be batchloaded, it was # already enriched. batch_loaded_commit = replacements.fetch(original_commit.id, original_commit) # If batch loading the commit failed, fall back to the original commit. # We need to explicitly check `.nil?` since otherwise a `BatchLoader` instance # that looks like `nil` is returned. batch_loaded_commit.nil? ? original_commit : batch_loaded_commit end self end def respond_to_missing?(message, inc_private = false) commits.respond_to?(message, inc_private) end # rubocop:disable GitlabSecurity/PublicSend def method_missing(message, *args, &block) commits.public_send(message, *args, &block) end def next_page @pagination.next_page end def load_tags oids = commits.map(&:id) references = repository.list_refs([Gitlab::Git::TAG_REF_PREFIX], pointing_at_oids: oids, peel_tags: true) oid_to_references = references.group_by { |reference| reference.peeled_target.presence || reference.target } return self if oid_to_references.empty? commits.each do |commit| grouped_references = oid_to_references[commit.id] next unless grouped_references commit.referenced_by = grouped_references.map(&:name) end self end end