summaryrefslogtreecommitdiff
path: root/lib/gitlab/tree_summary.rb
blob: 85f0ba1fd25b5e3d724964fbad189e2f70313e8f (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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# 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
        .tap { |summary| fill_last_commits!(summary) }

      [summary, commits]
    end

    def fetch_logs
      logs, _ = summarize

      new_offset = next_offset if more?

      [logs.as_json, new_offset]
    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

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

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

    def fill_last_commits!(entries)
      commits_hsh = fetch_last_cached_commits_list
      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 fetch_last_cached_commits_list
      cache_key = ['projects', project.id, 'last_commits', commit.id, ensured_path, offset, limit]

      commits = Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
        repository
          .list_last_commits_for_tree(commit.id, ensured_path, offset: offset, limit: limit, literal_pathspec: true)
          .transform_values! { |commit| commit_to_hash(commit) }
      end

      commits.transform_values! { |value| Commit.from_hash(value, project) }
    end

    def cache_commit(commit)
      return unless commit.present?

      resolved_commits[commit.id] ||= commit
    end

    def commit_to_hash(commit)
      commit.to_hash.tap do |hash|
        hash[:message] = hash[:message].to_s.truncate_bytes(1.kilobyte, omission: '...')
      end
    end

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

    def all_contents
      strong_memoize(:all_contents) { cached_contents }
    end

    def cached_contents
      cache_key = ['projects', project.id, 'content', commit.id, path]

      Rails.cache.fetch(cache_key, expires_in: CACHE_EXPIRE_IN) do
        [
          *tree.trees,
          *tree.blobs,
          *tree.submodules
        ].map { |entry| { file_name: entry.name, type: entry.type } }
      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_mod_with('Gitlab::TreeSummary')