1# frozen_string_literal: true
2
3module Gitlab
4  module Diff
5    module FileCollection
6      # Builds a paginated diff file collection and collects pagination
7      # metadata.
8      #
9      # It doesn't handle caching yet as we're not prepared to write/read
10      # separate file keys (https://gitlab.com/gitlab-org/gitlab/issues/30550).
11      #
12      class MergeRequestDiffBatch < MergeRequestDiffBase
13        DEFAULT_BATCH_PAGE = 1
14        DEFAULT_BATCH_SIZE = 30
15
16        attr_reader :pagination_data
17
18        def initialize(merge_request_diff, batch_page, batch_size, diff_options:)
19          super(merge_request_diff, diff_options: diff_options)
20
21          @paginated_collection = load_paginated_collection(batch_page, batch_size, diff_options)
22
23          @pagination_data = {
24            total_pages: @paginated_collection.blank? ? nil : relation.size
25          }
26        end
27
28        override :diffs
29        def diffs
30          strong_memoize(:diffs) do
31            @merge_request_diff.opening_external_diff do
32              # Avoiding any extra queries.
33              collection = @paginated_collection.to_a
34
35              # The offset collection and calculation is required so that we
36              # know how much has been loaded in previous batches, collapsing
37              # the current paginated set accordingly (collection limit calculation).
38              # See: https://docs.gitlab.com/ee/development/diffs.html#diff-collection-limits
39              #
40              offset_index = collection.first&.index
41              options = diff_options.dup
42
43              collection =
44                if offset_index && offset_index > 0
45                  offset_collection = relation.limit(offset_index) # rubocop:disable CodeReuse/ActiveRecord
46                  options[:offset_index] = offset_index
47                  offset_collection + collection
48                else
49                  collection
50                end
51
52              Gitlab::Git::DiffCollection.new(collection.map(&:to_hash), options)
53            end
54          end
55        end
56
57        private
58
59        def relation
60          @merge_request_diff.merge_request_diff_files
61        end
62
63        # rubocop: disable CodeReuse/ActiveRecord
64        def load_paginated_collection(batch_page, batch_size, diff_options)
65          batch_page ||= DEFAULT_BATCH_PAGE
66          batch_size ||= DEFAULT_BATCH_SIZE
67
68          paths = diff_options&.fetch(:paths, nil)
69
70          paginated_collection = relation.offset(batch_page).limit([batch_size.to_i, DEFAULT_BATCH_SIZE].min)
71          paginated_collection = paginated_collection.by_paths(paths) if paths
72
73          paginated_collection
74        end
75        # rubocop: enable CodeReuse/ActiveRecord
76      end
77    end
78  end
79end
80