Commit 156ce433 authored by Patrick Steinhardt's avatar Patrick Steinhardt Committed by Tetiana Chupryna

checks: Implement infrastructure for bulk diff checks

parent 106f6bec
---
name: changes_batch_commits
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66731
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336992
milestone: '14.2'
type: development
group: group::gitaly
default_enabled: false
...@@ -77,7 +77,7 @@ RSpec.describe Gitlab::GitAccess do ...@@ -77,7 +77,7 @@ RSpec.describe Gitlab::GitAccess do
end end
it 'allows githook for new branch with an old bad commit' do it 'allows githook for new branch with an old bad commit' do
bad_commit = double("Commit", safe_message: 'Some change').as_null_object bad_commit = double("Commit", safe_message: 'Some change', id: end_sha).as_null_object
ref_object = double(name: 'heads/master') ref_object = double(name: 'heads/master')
allow(bad_commit).to receive(:refs).and_return([ref_object]) allow(bad_commit).to receive(:refs).and_return([ref_object])
allow_next_instance_of(Repository) do |instance| allow_next_instance_of(Repository) do |instance|
...@@ -91,7 +91,7 @@ RSpec.describe Gitlab::GitAccess do ...@@ -91,7 +91,7 @@ RSpec.describe Gitlab::GitAccess do
end end
it 'allows githook for any change with an old bad commit' do it 'allows githook for any change with an old bad commit' do
bad_commit = double("Commit", safe_message: 'Some change').as_null_object bad_commit = double("Commit", safe_message: 'Some change', id: end_sha).as_null_object
ref_object = double(name: 'heads/master') ref_object = double(name: 'heads/master')
allow(bad_commit).to receive(:refs).and_return([ref_object]) allow(bad_commit).to receive(:refs).and_return([ref_object])
allow(project.repository).to receive(:commits_between).and_return([bad_commit]) allow(project.repository).to receive(:commits_between).and_return([bad_commit])
...@@ -103,7 +103,7 @@ RSpec.describe Gitlab::GitAccess do ...@@ -103,7 +103,7 @@ RSpec.describe Gitlab::GitAccess do
end end
it 'does not allow any change from Web UI with bad commit' do it 'does not allow any change from Web UI with bad commit' do
bad_commit = double("Commit", safe_message: 'Some change').as_null_object bad_commit = double("Commit", safe_message: 'Some change', id: end_sha).as_null_object
# We use tmp ref a a temporary for Web UI commiting # We use tmp ref a a temporary for Web UI commiting
ref_object = double(name: 'refs/tmp') ref_object = double(name: 'refs/tmp')
allow(bad_commit).to receive(:refs).and_return([ref_object]) allow(bad_commit).to receive(:refs).and_return([ref_object])
......
...@@ -29,11 +29,48 @@ module Gitlab ...@@ -29,11 +29,48 @@ module Gitlab
true true
end end
# All commits which have been newly introduced via any of the given
# changes. This set may also contain commits which are not referenced by
# any of the new revisions.
def commits
newrevs = @changes.map do |change|
newrev = change[:newrev]
newrev unless newrev.blank? || Gitlab::Git.blank_ref?(newrev)
end.compact
return [] if newrevs.empty?
@commits ||= project.repository.new_commits(newrevs)
end
# All commits which have been newly introduced via the given revision.
def commits_for(newrev)
commits_by_id = commits.index_by(&:id)
result = []
pending = [newrev]
# We go up the parent chain of our newrev and collect all commits which
# are new. In case a commit's ID cannot be found in the set of new
# commits, then it must already be a preexisting commit.
pending.each do |rev|
commit = commits_by_id[rev]
next if commit.nil?
pending.push(*commit.parent_ids)
result << commit
end
result
end
protected protected
def single_access_checks! def single_access_checks!
# Iterate over all changes to find if user allowed all of them to be applied # Iterate over all changes to find if user allowed all of them to be applied
changes.each do |change| changes.each do |change|
commits = Gitlab::Lazy.new { commits_for(change[:newrev]) } if Feature.enabled?(:changes_batch_commits)
# If user does not have access to make at least one change, cancel all # If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up # push by allowing the exception to bubble up
Checks::SingleChangeAccess.new( Checks::SingleChangeAccess.new(
...@@ -41,7 +78,8 @@ module Gitlab ...@@ -41,7 +78,8 @@ module Gitlab
user_access: user_access, user_access: user_access,
project: project, project: project,
protocol: protocol, protocol: protocol,
logger: logger logger: logger,
commits: commits
).validate! ).validate!
end end
end end
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
def initialize( def initialize(
change, user_access:, project:, change, user_access:, project:,
protocol:, logger: protocol:, logger:, commits: nil
) )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref) @branch_name = Gitlab::Git.branch_name(@ref)
...@@ -19,6 +19,7 @@ module Gitlab ...@@ -19,6 +19,7 @@ module Gitlab
@user_access = user_access @user_access = user_access
@project = project @project = project
@protocol = protocol @protocol = protocol
@commits = commits
@logger = logger @logger = logger
@logger.append_message("Running checks for ref: #{@branch_name || @tag_name}") @logger.append_message("Running checks for ref: #{@branch_name || @tag_name}")
......
...@@ -3,15 +3,16 @@ ...@@ -3,15 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Checks::ChangesAccess do RSpec.describe Gitlab::Checks::ChangesAccess do
describe '#validate!' do
include_context 'changes access checks context' include_context 'changes access checks context'
subject { changes_access }
describe '#validate!' do
shared_examples '#validate!' do
before do before do
allow(project).to receive(:lfs_enabled?).and_return(true) allow(project).to receive(:lfs_enabled?).and_return(true)
end end
subject { changes_access }
context 'without failed checks' do context 'without failed checks' do
it "doesn't raise an error" do it "doesn't raise an error" do
expect { subject.validate! }.not_to raise_error expect { subject.validate! }.not_to raise_error
...@@ -39,4 +40,132 @@ RSpec.describe Gitlab::Checks::ChangesAccess do ...@@ -39,4 +40,132 @@ RSpec.describe Gitlab::Checks::ChangesAccess do
end end
end end
end end
context 'with batched commits enabled' do
before do
stub_feature_flags(changes_batch_commits: true)
end
it_behaves_like '#validate!'
end
context 'with batched commits disabled' do
before do
stub_feature_flags(changes_batch_commits: false)
end
it_behaves_like '#validate!'
end
end
describe '#commits' do
it 'calls #new_commits' do
expect(project.repository).to receive(:new_commits).and_return([])
expect(subject.commits).to eq([])
end
context 'when changes contain empty revisions' do
let(:changes) { [{ newrev: newrev }, { newrev: '' }, { newrev: Gitlab::Git::BLANK_SHA }] }
let(:expected_commit) { instance_double(Commit) }
it 'returns only commits with non empty revisions' do
expect(project.repository).to receive(:new_commits).with([newrev]) { [expected_commit] }
expect(subject.commits).to eq([expected_commit])
end
end
end
describe '#commits_for' do
let(:new_commits) { [] }
let(:expected_commits) { [] }
shared_examples 'a listing of new commits' do
it 'returns expected commits' do
expect(subject).to receive(:commits).and_return(new_commits)
expect(subject.commits_for(newrev)).to eq(expected_commits)
end
end
context 'with no commits' do
it_behaves_like 'a listing of new commits'
end
context 'with unrelated commits' do
let(:new_commits) { [create_commit('1234', %w[1111 2222])] }
it_behaves_like 'a listing of new commits'
end
context 'with single related commit' do
let(:new_commits) { [create_commit(newrev, %w[1111 2222])] }
let(:expected_commits) { new_commits }
it_behaves_like 'a listing of new commits'
end
context 'with single related and unrelated commit' do
let(:new_commits) do
[
create_commit(newrev, %w[1111 2222]),
create_commit('abcd', %w[1111 2222])
]
end
let(:expected_commits) do
[create_commit(newrev, %w[1111 2222])]
end
it_behaves_like 'a listing of new commits'
end
context 'with multiple related commits' do
let(:new_commits) do
[
create_commit(newrev, %w[1111]),
create_commit('1111', %w[2222]),
create_commit('abcd', [])
]
end
let(:expected_commits) do
[
create_commit(newrev, %w[1111]),
create_commit('1111', %w[2222])
]
end
it_behaves_like 'a listing of new commits'
end
context 'with merge commits' do
let(:new_commits) do
[
create_commit(newrev, %w[1111 2222 3333]),
create_commit('1111', []),
create_commit('3333', %w[4444]),
create_commit('4444', [])
]
end
let(:expected_commits) do
[
create_commit(newrev, %w[1111 2222 3333]),
create_commit('1111', []),
create_commit('3333', %w[4444]),
create_commit('4444', [])
]
end
it_behaves_like 'a listing of new commits'
end
end
def create_commit(id, parent_ids)
Gitlab::Git::Commit.new(project.repository, {
id: id,
parent_ids: parent_ids
})
end
end end
...@@ -58,5 +58,52 @@ RSpec.describe Gitlab::Checks::SingleChangeAccess do ...@@ -58,5 +58,52 @@ RSpec.describe Gitlab::Checks::SingleChangeAccess do
expect { access.validate! }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError) expect { access.validate! }.to raise_error(Gitlab::Checks::TimedLogger::TimeoutError)
end end
end end
describe '#commits' do
let(:expected_commits) { [Gitlab::Git::Commit.new(project.repository, { id: "1234" })] }
let(:access) do
described_class.new(changes,
project: project,
user_access: user_access,
protocol: protocol,
logger: logger,
commits: provided_commits)
end
shared_examples '#commits' do
it 'returns expected commits' do
expect(access.commits).to eq(expected_commits)
end
it 'returns expected commits on repeated calls' do
expect(access.commits).to eq(expected_commits)
expect(access.commits).to eq(expected_commits)
end
end
context 'with provided commits' do
let(:provided_commits) { expected_commits }
before do
expect(project.repository).not_to receive(:new_commits)
end
it_behaves_like '#commits'
end
context 'without provided commits' do
let(:provided_commits) { nil }
before do
expect(project.repository)
.to receive(:new_commits)
.once
.and_return(expected_commits)
end
it_behaves_like '#commits'
end
end
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment