summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue6
-rw-r--r--app/controllers/projects/merge_requests/application_controller.rb6
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb27
-rw-r--r--app/controllers/projects/merge_requests_controller.rb3
-rw-r--r--app/models/merge_request_diff.rb7
-rw-r--r--app/models/merge_request_diff_file.rb1
-rw-r--r--app/serializers/paginated_diff_entity.rb50
-rw-r--r--app/serializers/paginated_diff_serializer.rb5
-rw-r--r--changelogs/unreleased/31290-mr-diffs-batch-load.yml5
-rw-r--r--config/routes/project.rb1
-rw-r--r--db/migrate/20191004133612_create_analytics_repository_file_commits.rb19
-rw-r--r--db/post_migrate/20191004134055_drop_unused_analytics_repository_file_edits.rb32
-rw-r--r--db/schema.rb16
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb14
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb21
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_batch.rb69
-rw-r--r--lib/gitlab/git/diff_collection.rb11
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb161
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb126
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb64
-rw-r--r--spec/serializers/paginated_diff_entity_spec.rb33
-rw-r--r--spec/support/shared_examples/diff_file_collections.rb8
23 files changed, 623 insertions, 64 deletions
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 9c258c4651f..13795eff714 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -167,7 +167,7 @@ dropdown-menu-labels dropdown-menu-selectable"
<div class="dropdown-page-one">
<dropdown-header v-if="showCreate" />
<dropdown-search-input />
- <div class="dropdown-content"></div>
+ <div class="dropdown-content" data-qa-selector="labels_dropdown_content"></div>
<div class="dropdown-loading"><gl-loading-icon /></div>
<dropdown-footer
v-if="showCreate"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
index cb53273c786..574b63cf8a6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -14,7 +14,11 @@ export default {
{{ __('Labels') }}
<template v-if="canEdit">
<i aria-hidden="true" class="fa fa-spinner fa-spin block-loading" data-hidden="true"> </i>
- <button type="button" class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle">
+ <button
+ type="button"
+ class="edit-link btn btn-blank float-right js-sidebar-dropdown-toggle"
+ data-qa-selector="labels_edit_button"
+ >
{{ __('Edit') }}
</button>
</template>
diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb
index edffeb32203..b7e99cb7ed0 100644
--- a/app/controllers/projects/merge_requests/application_controller.rb
+++ b/app/controllers/projects/merge_requests/application_controller.rb
@@ -14,7 +14,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
end
def merge_request_includes(association)
- association.includes(:metrics, :assignees, author: :status) # rubocop:disable CodeReuse/ActiveRecord
+ association.includes(preloadable_mr_relations) # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ def preloadable_mr_relations
+ [:metrics, :assignees, { author: :status }]
end
def merge_request_params
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 9c5caf7719e..23ef9157363 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -5,9 +5,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
include RendersNotes
before_action :apply_diff_view_cookie!
- before_action :commit
- before_action :define_diff_vars
- before_action :define_diff_comment_vars
+ before_action :commit, except: :diffs_batch
+ before_action :define_diff_vars, except: :diffs_batch
+ before_action :define_diff_comment_vars, except: :diffs_batch
def show
render_diffs
@@ -17,8 +17,29 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
render_diffs
end
+ def diffs_batch
+ return render_404 unless Feature.enabled?(:diffs_batch_load, @merge_request.project)
+
+ diffable = @merge_request.merge_request_diff
+
+ return render_404 unless diffable
+
+ diffs = diffable.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options)
+
+ options = {
+ merge_request: @merge_request,
+ pagination_data: diffs.pagination_data
+ }
+
+ render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
+ end
+
private
+ def preloadable_mr_relations
+ [{ source_project: :namespace }, { target_project: :namespace }]
+ end
+
def render_diffs
@environment = @merge_request.environments_for(current_user).last
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 7ddff9c1893..bd22226da5c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -17,6 +17,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :set_issuables_index, only: [:index]
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
+ before_action only: [:show] do
+ push_frontend_feature_flag(:diffs_batch_load, @project)
+ end
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 8b5f10ce159..7706119681d 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -297,6 +297,13 @@ class MergeRequestDiff < ApplicationRecord
base_commit_sha? && head_commit_sha? && start_commit_sha?
end
+ def diffs_in_batch(batch_page, batch_size, diff_options:)
+ Gitlab::Diff::FileCollection::MergeRequestDiffBatch.new(self,
+ batch_page,
+ batch_size,
+ diff_options: diff_options)
+ end
+
def diffs(diff_options = nil)
if without_files? && comparison = diff_refs&.compare_in(project)
# It should fetch the repository when diffs are cleaned by the system.
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index a532c1e6356..14c86ec69da 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -5,6 +5,7 @@ class MergeRequestDiffFile < ApplicationRecord
include DiffFile
belongs_to :merge_request_diff, inverse_of: :merge_request_diff_files
+ alias_attribute :index, :relative_order
def utf8_diff
return '' if diff.blank?
diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb
new file mode 100644
index 00000000000..622da926c69
--- /dev/null
+++ b/app/serializers/paginated_diff_entity.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+# Serializes diffs with pagination data.
+#
+# Avoid adding more keys to this serializer as processing the
+# diff file serialization is not cheap.
+#
+class PaginatedDiffEntity < Grape::Entity
+ include RequestAwareEntity
+
+ expose :diff_files do |diffs, options|
+ submodule_links = Gitlab::SubmoduleLinks.new(merge_request.project.repository)
+ DiffFileEntity.represent(diffs.diff_files, options.merge(submodule_links: submodule_links))
+ end
+
+ expose :pagination do
+ expose :current_page
+ expose :next_page
+ expose :total_pages
+ expose :next_page_href do |diffs|
+ next unless next_page
+
+ project = merge_request.target_project
+
+ diffs_batch_namespace_project_json_merge_request_path(
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ page: next_page,
+ format: :json
+ )
+ end
+ end
+
+ private
+
+ %i[current_page next_page total_pages].each do |method|
+ define_method method do
+ pagination_data[method]
+ end
+ end
+
+ def pagination_data
+ options.fetch(:pagination_data, {})
+ end
+
+ def merge_request
+ options[:merge_request]
+ end
+end
diff --git a/app/serializers/paginated_diff_serializer.rb b/app/serializers/paginated_diff_serializer.rb
new file mode 100644
index 00000000000..9b40fbf7843
--- /dev/null
+++ b/app/serializers/paginated_diff_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class PaginatedDiffSerializer < BaseSerializer
+ entity PaginatedDiffEntity
+end
diff --git a/changelogs/unreleased/31290-mr-diffs-batch-load.yml b/changelogs/unreleased/31290-mr-diffs-batch-load.yml
new file mode 100644
index 00000000000..933493e80d7
--- /dev/null
+++ b/changelogs/unreleased/31290-mr-diffs-batch-load.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce diffs_batch JSON endpoint for paginated diffs
+merge_request: 17651
+author:
+type: added
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 3675a4600fe..f9331a22bc8 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -281,6 +281,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get :commits
get :pipelines
get :diffs, to: 'merge_requests/diffs#show'
+ get :diffs_batch, to: 'merge_requests/diffs#diffs_batch'
get :widget, to: 'merge_requests/content#widget'
get :cached_widget, to: 'merge_requests/content#cached_widget'
end
diff --git a/db/migrate/20191004133612_create_analytics_repository_file_commits.rb b/db/migrate/20191004133612_create_analytics_repository_file_commits.rb
new file mode 100644
index 00000000000..f2064b2b301
--- /dev/null
+++ b/db/migrate/20191004133612_create_analytics_repository_file_commits.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class CreateAnalyticsRepositoryFileCommits < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :analytics_repository_file_commits do |t|
+ t.references :analytics_repository_file, index: { name: 'index_analytics_repository_file_commits_file_id' }, foreign_key: { on_delete: :cascade }, null: false
+ t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
+ t.date :committed_date, null: false
+ t.integer :commit_count, limit: 2, null: false
+ end
+
+ add_index :analytics_repository_file_commits,
+ [:project_id, :committed_date, :analytics_repository_file_id],
+ name: 'index_file_commits_on_committed_date_file_id_and_project_id',
+ unique: true
+ end
+end
diff --git a/db/post_migrate/20191004134055_drop_unused_analytics_repository_file_edits.rb b/db/post_migrate/20191004134055_drop_unused_analytics_repository_file_edits.rb
new file mode 100644
index 00000000000..7bf92080fe6
--- /dev/null
+++ b/db/post_migrate/20191004134055_drop_unused_analytics_repository_file_edits.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class DropUnusedAnalyticsRepositoryFileEdits < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def up
+ # The table was never used, there is no app code that writes or reads the table. Safe to remove.
+ drop_table :analytics_repository_file_edits
+ end
+
+ def down
+ create_table :analytics_repository_file_edits do |t|
+ t.references :project,
+ index: true,
+ foreign_key: { on_delete: :cascade }, null: false
+ t.references :analytics_repository_file,
+ index: false,
+ foreign_key: { on_delete: :cascade },
+ null: false
+ t.date :committed_date,
+ null: false
+ t.integer :num_edits,
+ null: false,
+ default: 0
+ end
+
+ add_index :analytics_repository_file_edits,
+ [:analytics_repository_file_id, :committed_date, :project_id],
+ name: 'index_file_edits_on_committed_date_file_id_and_project_id',
+ unique: true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3a1d5d73c89..352023ac40d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_09_30_025655) do
+ActiveRecord::Schema.define(version: 2019_10_04_134055) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -93,13 +93,13 @@ ActiveRecord::Schema.define(version: 2019_09_30_025655) do
t.index ["project_id"], name: "analytics_repository_languages_on_project_id"
end
- create_table "analytics_repository_file_edits", force: :cascade do |t|
- t.bigint "project_id", null: false
+ create_table "analytics_repository_file_commits", force: :cascade do |t|
t.bigint "analytics_repository_file_id", null: false
+ t.bigint "project_id", null: false
t.date "committed_date", null: false
- t.integer "num_edits", default: 0, null: false
- t.index ["analytics_repository_file_id", "committed_date", "project_id"], name: "index_file_edits_on_committed_date_file_id_and_project_id", unique: true
- t.index ["project_id"], name: "index_analytics_repository_file_edits_on_project_id"
+ t.integer "commit_count", limit: 2, null: false
+ t.index ["analytics_repository_file_id"], name: "index_analytics_repository_file_commits_file_id"
+ t.index ["project_id", "committed_date", "analytics_repository_file_id"], name: "index_file_commits_on_committed_date_file_id_and_project_id", unique: true
end
create_table "analytics_repository_files", force: :cascade do |t|
@@ -3894,8 +3894,8 @@ ActiveRecord::Schema.define(version: 2019_09_30_025655) do
add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade
add_foreign_key "analytics_language_trend_repository_languages", "programming_languages", on_delete: :cascade
add_foreign_key "analytics_language_trend_repository_languages", "projects", on_delete: :cascade
- add_foreign_key "analytics_repository_file_edits", "analytics_repository_files", on_delete: :cascade
- add_foreign_key "analytics_repository_file_edits", "projects", on_delete: :cascade
+ add_foreign_key "analytics_repository_file_commits", "analytics_repository_files", on_delete: :cascade
+ add_foreign_key "analytics_repository_file_commits", "projects", on_delete: :cascade
add_foreign_key "analytics_repository_files", "projects", on_delete: :cascade
add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify
add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index e29bf75f341..c4288ca6408 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -3,19 +3,7 @@
module Gitlab
module Diff
module FileCollection
- class MergeRequestDiff < Base
- extend ::Gitlab::Utils::Override
-
- def initialize(merge_request_diff, diff_options:)
- @merge_request_diff = merge_request_diff
-
- super(merge_request_diff,
- project: merge_request_diff.project,
- diff_options: diff_options,
- diff_refs: merge_request_diff.diff_refs,
- fallback_diff_refs: merge_request_diff.fallback_diff_refs)
- end
-
+ class MergeRequestDiff < MergeRequestDiffBase
def diff_files
diff_files = super
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
new file mode 100644
index 00000000000..a747a6ed475
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ module FileCollection
+ class MergeRequestDiffBase < Base
+ extend ::Gitlab::Utils::Override
+
+ def initialize(merge_request_diff, diff_options:)
+ @merge_request_diff = merge_request_diff
+
+ super(merge_request_diff,
+ project: merge_request_diff.project,
+ diff_options: diff_options,
+ diff_refs: merge_request_diff.diff_refs,
+ fallback_diff_refs: merge_request_diff.fallback_diff_refs)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb b/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb
new file mode 100644
index 00000000000..c6d1e0b93a7
--- /dev/null
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_batch.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Diff
+ module FileCollection
+ # Builds a paginated diff file collection and collects pagination
+ # metadata.
+ #
+ # It doesn't handle caching yet as we're not prepared to write/read
+ # separate file keys (https://gitlab.com/gitlab-org/gitlab/issues/30550).
+ #
+ class MergeRequestDiffBatch < MergeRequestDiffBase
+ DEFAULT_BATCH_PAGE = 1
+ DEFAULT_BATCH_SIZE = 20
+
+ attr_reader :pagination_data
+
+ def initialize(merge_request_diff, batch_page, batch_size, diff_options:)
+ super(merge_request_diff, diff_options: diff_options)
+
+ batch_page ||= DEFAULT_BATCH_PAGE
+ batch_size ||= DEFAULT_BATCH_SIZE
+
+ @paginated_collection = relation.page(batch_page).per(batch_size)
+ @pagination_data = {
+ current_page: @paginated_collection.current_page,
+ next_page: @paginated_collection.next_page,
+ total_pages: @paginated_collection.total_pages
+ }
+ end
+
+ override :diffs
+ def diffs
+ strong_memoize(:diffs) do
+ @merge_request_diff.opening_external_diff do
+ # Avoiding any extra queries.
+ collection = @paginated_collection.to_a
+
+ # The offset collection and calculation is required so that we
+ # know how much has been loaded in previous batches, collapsing
+ # the current paginated set accordingly (collection limit calculation).
+ # See: https://docs.gitlab.com/ee/development/diffs.html#diff-collection-limits
+ #
+ offset_index = collection.first&.index
+ options = diff_options.dup
+
+ collection =
+ if offset_index && offset_index > 0
+ offset_collection = relation.limit(offset_index) # rubocop:disable CodeReuse/ActiveRecord
+ options[:offset_index] = offset_index
+ offset_collection + collection
+ else
+ collection
+ end
+
+ Gitlab::Git::DiffCollection.new(collection.map(&:to_hash), options)
+ end
+ end
+ end
+
+ private
+
+ def relation
+ @merge_request_diff.merge_request_diff_files
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index cb9154cb1e8..b79e30bff78 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -31,6 +31,7 @@ module Gitlab
@limits = self.class.limits(options)
@enforce_limits = !!options.fetch(:limits, true)
@expanded = !!options.fetch(:expanded, true)
+ @offset_index = options.fetch(:offset_index, 0)
@line_count = 0
@byte_count = 0
@@ -128,7 +129,7 @@ module Gitlab
def each_serialized_patch
i = @array.length
- @iterator.each do |raw|
+ @iterator.each_with_index do |raw, iterator_index|
@empty = false
if @enforce_limits && i >= max_files
@@ -154,8 +155,12 @@ module Gitlab
break
end
- yield @array[i] = diff
- i += 1
+ # We should not yield / memoize diffs before the offset index. Though,
+ # we still consider the limit buffers for diffs before it.
+ if iterator_index >= @offset_index
+ yield @array[i] = diff
+ i += 1
+ end
end
end
end
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index ac3e9901123..ca3c802777b 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -5,6 +5,49 @@ require 'spec_helper'
describe Projects::MergeRequests::DiffsController do
include ProjectForksHelper
+ shared_examples 'forked project with submodules' do
+ render_views
+
+ let(:project) { create(:project, :repository) }
+ let(:forked_project) { fork_project_with_submodules(project) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+
+ before do
+ project.add_developer(user)
+
+ merge_request.reload
+ go
+ end
+
+ it 'renders' do
+ expect(response).to be_successful
+ expect(response.body).to have_content('Subproject commit')
+ end
+ end
+
+ shared_examples 'persisted preferred diff view cookie' do
+ context 'with view param' do
+ before do
+ go(view: 'parallel')
+ end
+
+ it 'saves the preferred diff view in a cookie' do
+ expect(response.cookies['diff_view']).to eq('parallel')
+ end
+ end
+
+ context 'when the user cannot view the merge request' do
+ before do
+ project.team.truncate
+ go
+ end
+
+ it 'returns a 404' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) }
@@ -51,36 +94,10 @@ describe Projects::MergeRequests::DiffsController do
end
end
- context 'with forked projects with submodules' do
- render_views
-
- let(:project) { create(:project, :repository) }
- let(:forked_project) { fork_project_with_submodules(project) }
- let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
-
- before do
- project.add_developer(user)
-
- merge_request.reload
- go
- end
-
- it 'renders' do
- expect(response).to be_successful
- expect(response.body).to have_content('Subproject commit')
- end
- end
+ it_behaves_like 'forked project with submodules'
end
- context 'with view' do
- before do
- go(view: 'parallel')
- end
-
- it 'saves the preferred diff view in a cookie' do
- expect(response.cookies['diff_view']).to eq('parallel')
- end
- end
+ it_behaves_like 'persisted preferred diff view cookie'
end
describe 'GET diff_for_path' do
@@ -154,4 +171,92 @@ describe Projects::MergeRequests::DiffsController do
end
end
end
+
+ describe 'GET diffs_batch' do
+ def go(extra_params = {})
+ params = {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.iid,
+ format: 'json'
+ }
+
+ get :diffs_batch, params: params.merge(extra_params)
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(diffs_batch_load: false)
+ end
+
+ it 'returns 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when not authorized' do
+ let(:other_user) { create(:user) }
+
+ before do
+ sign_in(other_user)
+ end
+
+ it 'returns 404' do
+ go
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with default params' do
+ let(:expected_options) do
+ {
+ merge_request: merge_request,
+ pagination_data: {
+ current_page: 1,
+ next_page: nil,
+ total_pages: 1
+ }
+ }
+ end
+
+ it 'serializes paginated merge request diff collection' do
+ expect_next_instance_of(PaginatedDiffSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch), expected_options)
+ .and_call_original
+ end
+
+ go
+ end
+ end
+
+ context 'with smaller diff batch params' do
+ let(:expected_options) do
+ {
+ merge_request: merge_request,
+ pagination_data: {
+ current_page: 2,
+ next_page: 3,
+ total_pages: 4
+ }
+ }
+ end
+
+ it 'serializes paginated merge request diff collection' do
+ expect_next_instance_of(PaginatedDiffSerializer) do |instance|
+ expect(instance).to receive(:represent)
+ .with(an_instance_of(Gitlab::Diff::FileCollection::MergeRequestDiffBatch), expected_options)
+ .and_call_original
+ end
+
+ go(page: 2, per_page: 5)
+ end
+ end
+
+ it_behaves_like 'forked project with submodules'
+ it_behaves_like 'persisted preferred diff view cookie'
+ end
end
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
new file mode 100644
index 00000000000..265c6260ca9
--- /dev/null
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_batch_spec.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::FileCollection::MergeRequestDiffBatch do
+ let(:merge_request) { create(:merge_request) }
+ let(:batch_page) { 1 }
+ let(:batch_size) { 10 }
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:diff_files_relation) { diffable.merge_request_diff_files }
+
+ subject do
+ described_class.new(diffable,
+ batch_page,
+ batch_size,
+ diff_options: nil)
+ end
+
+ let(:diff_files) { subject.diff_files }
+
+ describe 'initialize' do
+ it 'memoizes pagination_data' do
+ expect(subject.pagination_data).to eq(current_page: 1, next_page: 2, total_pages: 2)
+ end
+ end
+
+ describe '#diff_files' do
+ let(:batch_size) { 3 }
+ let(:paginated_rel) { diff_files_relation.page(batch_page).per(batch_size) }
+
+ let(:expected_batch_files) do
+ paginated_rel.map(&:new_path)
+ end
+
+ it 'returns paginated diff files' do
+ expect(diff_files.size).to eq(3)
+ end
+
+ it 'returns a valid instance of a DiffCollection' do
+ expect(diff_files).to be_a(Gitlab::Git::DiffCollection)
+ end
+
+ context 'first page' do
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'another page' do
+ let(:batch_page) { 2 }
+
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'nil batch_page' do
+ let(:batch_page) { nil }
+
+ it 'returns correct diff files' do
+ expected_batch_files =
+ diff_files_relation.page(described_class::DEFAULT_BATCH_PAGE).per(batch_size).map(&:new_path)
+
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'nil batch_size' do
+ let(:batch_size) { nil }
+
+ it 'returns correct diff files' do
+ expected_batch_files =
+ diff_files_relation.page(batch_page).per(described_class::DEFAULT_BATCH_SIZE).map(&:new_path)
+
+ expect(diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+
+ context 'invalid page' do
+ let(:batch_page) { 999 }
+
+ it 'returns correct diff files' do
+ expect(diff_files.map(&:new_path)).to be_empty
+ end
+ end
+
+ context 'last page' do
+ it 'returns correct diff files' do
+ last_page = paginated_rel.total_pages
+ collection = described_class.new(diffable,
+ last_page,
+ batch_size,
+ diff_options: nil)
+
+ expected_batch_files = diff_files_relation.page(last_page).per(batch_size).map(&:new_path)
+
+ expect(collection.diff_files.map(&:new_path)).to eq(expected_batch_files)
+ end
+ end
+ end
+
+ it_behaves_like 'unfoldable diff' do
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ batch_page,
+ batch_size,
+ diff_options: nil)
+ end
+ end
+
+ it_behaves_like 'diff statistics' do
+ let(:collection_default_args) do
+ { diff_options: {} }
+ end
+
+ let(:diffable) { merge_request.merge_request_diff }
+ let(:stub_path) { '.gitignore' }
+
+ subject do
+ described_class.new(merge_request.merge_request_diff,
+ batch_page,
+ batch_size,
+ collection_default_args)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index be6ab0c1200..ded173c49ef 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -537,6 +537,70 @@ describe Gitlab::Git::DiffCollection, :seed_helper do
end
end
end
+
+ context 'when offset_index is given' do
+ subject do
+ Gitlab::Git::DiffCollection.new(
+ iterator,
+ max_files: max_files,
+ max_lines: max_lines,
+ limits: limits,
+ offset_index: 2,
+ expanded: expanded
+ )
+ end
+
+ def diff(raw)
+ raw['diff']
+ end
+
+ let(:iterator) do
+ [
+ fake_diff(1, 1),
+ fake_diff(2, 2),
+ fake_diff(3, 3),
+ fake_diff(4, 4)
+ ]
+ end
+
+ it 'does not yield diffs before the offset' do
+ expect(subject.to_a.map(&:diff)).to eq(
+ [
+ diff(fake_diff(3, 3)),
+ diff(fake_diff(4, 4))
+ ]
+ )
+ end
+
+ context 'when go over safe limits on bytes' do
+ let(:iterator) do
+ [
+ fake_diff(1, 10), # 10
+ fake_diff(1, 10), # 20
+ fake_diff(1, 15), # 35
+ fake_diff(1, 20), # 55
+ fake_diff(1, 45), # 100 - limit hit
+ fake_diff(1, 45),
+ fake_diff(1, 20480),
+ fake_diff(1, 1)
+ ]
+ end
+
+ before do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS',
+ { max_files: max_files, max_lines: 80 })
+ end
+
+ it 'considers size of diffs before the offset for prunning' do
+ expect(subject.to_a.map(&:diff)).to eq(
+ [
+ diff(fake_diff(1, 15)),
+ diff(fake_diff(1, 20))
+ ]
+ )
+ end
+ end
+ end
end
def fake_diff(line_length, line_count)
diff --git a/spec/serializers/paginated_diff_entity_spec.rb b/spec/serializers/paginated_diff_entity_spec.rb
new file mode 100644
index 00000000000..7432e072318
--- /dev/null
+++ b/spec/serializers/paginated_diff_entity_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PaginatedDiffEntity do
+ let(:user) { create(:user) }
+ let(:request) { double('request', current_user: user) }
+ let(:merge_request) { create(:merge_request, :with_diffs) }
+ let(:diff_batch) { merge_request.merge_request_diff.diffs_in_batch(2, 3, diff_options: nil) }
+ let(:options) do
+ {
+ request: request,
+ merge_request: merge_request,
+ pagination_data: diff_batch.pagination_data
+ }
+ end
+ let(:entity) { described_class.new(diff_batch, options) }
+
+ subject { entity.as_json }
+
+ it 'exposes diff_files' do
+ expect(subject[:diff_files]).to be_present
+ end
+
+ it 'exposes pagination data' do
+ expect(subject[:pagination]).to eq(
+ current_page: 2,
+ next_page: 3,
+ next_page_href: "/#{merge_request.project.full_path}/merge_requests/#{merge_request.iid}/diffs_batch.json?page=3",
+ total_pages: 7
+ )
+ end
+end
diff --git a/spec/support/shared_examples/diff_file_collections.rb b/spec/support/shared_examples/diff_file_collections.rb
index 367ddf06c28..4c64abd2a97 100644
--- a/spec/support/shared_examples/diff_file_collections.rb
+++ b/spec/support/shared_examples/diff_file_collections.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
shared_examples 'diff statistics' do |test_include_stats_flag: true|
+ subject { described_class.new(diffable, collection_default_args) }
+
def stub_stats_find_by_path(path, stats_mock)
expect_next_instance_of(Gitlab::Git::DiffStatsCollection) do |collection|
allow(collection).to receive(:find_by_path).and_call_original
@@ -10,8 +12,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
context 'when should request diff stats' do
it 'Repository#diff_stats is called' do
- subject = described_class.new(diffable, collection_default_args)
-
expect(diffable.project.repository)
.to receive(:diff_stats)
.with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha)
@@ -21,8 +21,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
end
it 'Gitlab::Diff::File is initialized with diff stats' do
- subject = described_class.new(diffable, collection_default_args)
-
stats_mock = double(Gitaly::DiffStats, path: '.gitignore', additions: 758, deletions: 120)
stub_stats_find_by_path(stub_path, stats_mock)
@@ -37,8 +35,6 @@ shared_examples 'diff statistics' do |test_include_stats_flag: true|
it 'Repository#diff_stats is not called' do
collection_default_args[:diff_options][:include_stats] = false
- subject = described_class.new(diffable, collection_default_args)
-
expect(diffable.project.repository).not_to receive(:diff_stats)
subject.diff_files