Read about our upcoming Code of Conduct on this issue

commit_collection.rb 3.01 KB
Newer Older
1
2
# frozen_string_literal: true

3
# A collection of Commit instances for a specific container and Git reference.
4
5
class CommitCollection
  include Enumerable
6
  include Gitlab::Utils::StrongMemoize
7

8
  attr_reader :container, :ref, :commits
9

10
11
12
13
  delegate :repository, to: :container, allow_nil: true
  delegate :project, to: :repository, allow_nil: true

  # container - The object the commits belong to.
14
15
  # commits - The Commit instances to store.
  # ref - The name of the ref (e.g. "master").
16
17
  def initialize(container, commits, ref = nil)
    @container = container
18
19
20
21
22
23
24
25
    @commits = commits
    @ref = ref
  end

  def each(&block)
    commits.each(&block)
  end

26
27
  def committers
    emails = without_merge_commits.map(&:committer_email).uniq
28
29
30
31

    User.by_any_email(emails)
  end

32
33
  def without_merge_commits
    strong_memoize(:without_merge_commits) do
34
35
36
      # `#enrich!` the collection to ensure all commits contain
      # the necessary parent data
      enrich!.commits.reject(&:merge_commit?)
37
38
39
    end
  end

40
41
42
43
44
  # 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)
45
46
    return self unless project

47
48
49
50
51
52
53
54
55
    pipelines = project.ci_pipelines.latest_pipeline_per_commit(map(&:id), ref)

    each do |commit|
      commit.set_latest_pipeline_for_ref(ref, pipelines[commit.id])
    end

    self
  end

56
57
58
59
60
61
62
63
64
65
66
  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!
67
    # A container is needed in order to fetch data from gitaly. Containers
68
69
70
    # 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.
71
    return self if container.blank? || fully_enriched?
72
73
74
75

    # Batch load full Commits from the repository
    # and map to a Hash of id => Commit
    replacements = Hash[unenriched.map do |c|
76
      [c.id, Commit.lazy(container, c.id)]
77
78
79
    end.compact]

    # Replace the commits, keeping the same order
80
81
82
83
84
85
86
87
88
    @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
89
90
91
92
93
    end

    self
  end

94
95
96
97
98
99
100
101
102
  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
end