summaryrefslogtreecommitdiff
path: root/lib/gitlab/tree_summary.rb
blob: 4ec43e62c19e3d2e77686f9913d1fa315f5b6ed9 (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
# frozen_string_literal: true

module Gitlab
  class TreeSummary
    include ::Gitlab::Utils::StrongMemoize
    include ::MarkupHelper

    CACHE_EXPIRE_IN = 1.hour
    MAX_OFFSET = 2**31

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

    attr_reader :resolved_commits
    private :resolved_commits

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

      @path = params.fetch(:path, nil).presence
      @offset = [params.fetch(:offset, 0).to_i, MAX_OFFSET].min
      @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

    def fetch_logs
      cache_key = ['projects', project.id, 'logs', commit.id, path, offset]
      Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
        logs, _ = summarize

        new_offset = next_offset if more?

        [logs.as_json, new_offset]
      end
    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).force_encoding(Encoding::ASCII_8BIT)
    end

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

    def fill_last_commits!(entries)
      # Ensure the path is in "path/" format
      ensured_path =
        if path
          File.join(*[path, ""])
        end

      commits_hsh = repository.list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit)
      prerender_commit_full_titles!(commits_hsh.values)

      entries.each do |entry|
        path_key = entry_path(entry)
        commit = cache_commit(commits_hsh[path_key])

        if commit
          entry[:commit] = commit
          entry[:commit_path] = commit_path(commit)
          entry[:commit_title_html] = markdown_field(commit, :full_title)
        end
      end
    end

    def cache_commit(commit)
      return unless commit.present?

      resolved_commits[commit.id] ||= commit
    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

    def prerender_commit_full_titles!(commits)
      # Preload commit authors as they are used in rendering
      commits.each(&:lazy_author)

      renderer = Banzai::ObjectRenderer.new(user: user, default_project: project)
      renderer.render(commits, :full_title)
    end
  end
end

Gitlab::TreeSummary.prepend_if_ee('::EE::Gitlab::TreeSummary')