diff options
Diffstat (limited to 'lib/gitlab/tree_summary.rb')
-rw-r--r-- | lib/gitlab/tree_summary.rb | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/lib/gitlab/tree_summary.rb b/lib/gitlab/tree_summary.rb new file mode 100644 index 00000000000..b05d408b1c0 --- /dev/null +++ b/lib/gitlab/tree_summary.rb @@ -0,0 +1,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 |