summaryrefslogtreecommitdiff
path: root/lib/gitlab/tree_summary.rb
blob: b05d408b1c0f2a94e935deebc7020dce6cef29a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
module Gitlab
  class TreeSummary
    include ::Gitlab::Utils::StrongMemoize

    attr_reader :commit, :project, :path, :offset, :limit

    attr_reader :resolved_commits
    private :resolved_commits

    def initialize(commit, project, params = {})
      @commit = commit
      @project = project

      @path = params.fetch(:path, nil).presence
      @offset = params.fetch(:offset, 0).to_i
      @limit = (params.fetch(:limit, 25) || 25).to_i

      # Ensure that if multiple tree entries share the same last commit, they share
      # a ::Commit instance. This prevents us from rendering the same commit title
      # multiple times
      @resolved_commits = {}
    end

    # Creates a summary of the tree entries for a commit, within the window of
    # entries defined by the offset and limit parameters. This consists of two
    # return values:
    #
    #     - An Array of Hashes containing the following keys:
    #         - file_name:   The full path of the tree entry
    #         - type:        One of :blob, :tree, or :submodule
    #         - commit:      The last ::Commit to touch this entry in the tree
    #         - commit_path: URI of the commit in the web interface
    #     - An Array of the unique ::Commit objects in the first value
    def summarize
      summary = contents
        .map { |content| build_entry(content) }
        .tap { |summary| fill_last_commits!(summary) }

      [summary, commits]
    end

    # Does the tree contain more entries after the given offset + limit?
    def more?
      all_contents[next_offset].present?
    end

    # The offset of the next batch of tree entries. If more? returns false, this
    # batch will be empty
    def next_offset
      [all_contents.size + 1, offset + limit].min
    end

    private

    def contents
      all_contents[offset, limit]
    end

    def commits
      resolved_commits.values
    end

    def repository
      project.repository
    end

    def entry_path(entry)
      File.join(*[path, entry[:file_name]].compact)
    end

    def build_entry(entry)
      { file_name: entry.name, type: entry.type }
    end

    def fill_last_commits!(entries)
      # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433
      Gitlab::GitalyClient.allow_n_plus_1_calls do
        entries.each do |entry|
          raw_commit = repository.last_commit_for_path(commit.id, entry_path(entry))

          if raw_commit
            commit = resolve_commit(raw_commit)

            entry[:commit] = commit
            entry[:commit_path] =  commit_path(commit)
          end
        end
      end
    end

    def resolve_commit(raw_commit)
      return nil unless raw_commit.present?

      resolved_commits[raw_commit.id] ||= ::Commit.new(raw_commit, project)
    end

    def commit_path(commit)
      Gitlab::Routing.url_helpers.project_commit_path(project, commit)
    end

    def all_contents
      strong_memoize(:all_contents) do
        [
          *tree.trees,
          *tree.blobs,
          *tree.submodules
        ]
      end
    end

    def tree
      strong_memoize(:tree) { repository.tree(commit.id, path) }
    end
  end
end