Commit a3c102b8 authored by Stan Hu's avatar Stan Hu

Merge branch '227572-n-1-commitdelta-for-large-pushes-2' into 'master'

Resolve "N + 1: CommitDelta for large pushes"

See merge request gitlab-org/gitlab!46116
parents 67751440 f876eb84
...@@ -466,7 +466,7 @@ group :ed25519 do ...@@ -466,7 +466,7 @@ group :ed25519 do
end end
# Gitaly GRPC protocol definitions # Gitaly GRPC protocol definitions
gem 'gitaly', '~> 13.6.1' gem 'gitaly', '~> 13.7.0.pre.rc1'
gem 'grpc', '~> 1.30.2' gem 'grpc', '~> 1.30.2'
......
...@@ -422,7 +422,7 @@ GEM ...@@ -422,7 +422,7 @@ GEM
rails (>= 3.2.0) rails (>= 3.2.0)
git (1.7.0) git (1.7.0)
rchardet (~> 1.8) rchardet (~> 1.8)
gitaly (13.6.1) gitaly (13.7.0.pre.rc1)
grpc (~> 1.0) grpc (~> 1.0)
github-markup (1.7.0) github-markup (1.7.0)
gitlab-chronic (0.10.5) gitlab-chronic (0.10.5)
...@@ -1349,7 +1349,7 @@ DEPENDENCIES ...@@ -1349,7 +1349,7 @@ DEPENDENCIES
gettext (~> 3.3) gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly (~> 13.6.1) gitaly (~> 13.7.0.pre.rc1)
github-markup (~> 1.7.0) github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5) gitlab-chronic (~> 0.10.5)
gitlab-fog-azure-rm (~> 1.0) gitlab-fog-azure-rm (~> 1.0)
......
---
name: diff_check_with_paths_changed_rpc
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46116
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/288827
milestone: '13.7'
type: development
group: group::code review
default_enabled: false
...@@ -58,10 +58,14 @@ module EE ...@@ -58,10 +58,14 @@ module EE
def path_locks_validation def path_locks_validation
lambda do |diff| lambda do |diff|
path = if diff.renamed_file? path = if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project)
diff.old_path diff.path
else else
diff.new_path || diff.old_path if diff.renamed_file?
diff.old_path
else
diff.new_path || diff.old_path
end
end end
lock_info = project.find_path_lock(path) lock_info = project.find_path_lock(path)
...@@ -72,12 +76,24 @@ module EE ...@@ -72,12 +76,24 @@ module EE
end end
end end
def new_file?(path)
path.status == :ADDED
end
def file_name_validation def file_name_validation
lambda do |diff| lambda do |diff|
if (diff.renamed_file || diff.new_file) && blacklisted_regex = push_rule.filename_denylisted?(diff.new_path) if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project)
return unless blacklisted_regex.present? if new_file?(diff) && denylisted_regex = push_rule.filename_denylisted?(diff.path)
return unless denylisted_regex.present?
"File name #{diff.new_path} was blacklisted by the pattern #{blacklisted_regex}."
"File name #{diff.path} was blacklisted by the pattern #{denylisted_regex}."
end
else
if (diff.renamed_file || diff.new_file) && denylisted_regex = push_rule.filename_denylisted?(diff.new_path)
return unless denylisted_regex.present?
"File name #{diff.new_path} was blacklisted by the pattern #{denylisted_regex}."
end
end end
rescue ::PushRule::MatchError => e rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::ForbiddenError, e.message raise ::Gitlab::GitAccess::ForbiddenError, e.message
......
...@@ -5,61 +5,37 @@ require 'spec_helper' ...@@ -5,61 +5,37 @@ require 'spec_helper'
RSpec.describe Gitlab::Checks::DiffCheck do RSpec.describe Gitlab::Checks::DiffCheck do
include FakeBlobHelpers include FakeBlobHelpers
include_context 'push rules checks context' shared_examples_for "diff check" do
include_context 'push rules checks context'
describe '#validate!' do describe '#validate!' do
let(:push_allowed) { false } let(:push_allowed) { false }
before do
allow(user_access).to receive(:can_push_to_branch?).and_return(push_allowed)
end
shared_examples_for "returns codeowners validation message" do before do
it "returns an error message" do allow(user_access).to receive(:can_push_to_branch?).and_return(push_allowed)
expect(validation_result).to include("Pushes to protected branches")
end end
end
context 'no push rules active' do
let_it_be(:push_rule) { create(:push_rule) }
it "does not attempt to check commits" do shared_examples_for "returns codeowners validation message" do
expect(subject).not_to receive(:process_commits) it "returns an error message" do
expect(validation_result).to include("Pushes to protected branches")
subject.validate! end
end end
end
describe '#validate_code_owners?' do
let_it_be(:push_rule) { create(:push_rule, file_name_regex: 'READ*') }
let(:validate_code_owners) { subject.send(:validate_code_owners?) }
let(:protocol) { 'ssh' }
let(:push_allowed) { false }
context 'when user can not push to the branch' do
context 'when not updated from web' do
it 'checks if the branch requires code owner approval' do
expect(project).to receive(:branch_requires_code_owner_approval?).and_return(true)
expect(validate_code_owners).to eq(true) context 'no push rules active' do
end let_it_be(:push_rule) { create(:push_rule) }
end
context 'when updated from the web' do it "does not attempt to check commits" do
let(:protocol) { 'web' } expect(subject).not_to receive(:process_commits)
it 'returns false' do subject.validate!
expect(validate_code_owners).to eq(false)
end
end end
end end
context 'when a user can push to the branch' do describe '#validate_code_owners?' do
let(:push_allowed) { true } let_it_be(:push_rule) { create(:push_rule, file_name_regex: 'READ*') }
let(:validate_code_owners) { subject.send(:validate_code_owners?) }
it 'returns false' do let(:protocol) { 'ssh' }
expect(validate_code_owners).to eq(false) let(:push_allowed) { false }
end
context 'when push_rules_supersede_code_owners is disabled' do context 'when push_rules_supersede_code_owners is disabled' do
before do before do
...@@ -72,270 +48,306 @@ RSpec.describe Gitlab::Checks::DiffCheck do ...@@ -72,270 +48,306 @@ RSpec.describe Gitlab::Checks::DiffCheck do
expect(validate_code_owners).to eq(true) expect(validate_code_owners).to eq(true)
end end
end end
end
end
describe "#validate_code_owners" do context 'when user can not push to the branch' do
let!(:code_owner) { create(:user, username: "owner-1") } context 'when not updated from web' do
let(:project) { create(:project, :repository) } it 'checks if the branch requires code owner approval' do
let(:codeowner_content) { "*.rb @#{code_owner.username}\ndocs/CODEOWNERS @owner-1\n*.js.coffee @owner-1" } expect(project).to receive(:branch_requires_code_owner_approval?).and_return(true)
let(:codeowner_blob) { fake_blob(path: "CODEOWNERS", data: codeowner_content) }
let(:codeowner_blob_ref) { fake_blob(path: "CODEOWNERS", data: codeowner_content) }
let(:codeowner_lookup_ref) { merge_request.target_branch }
let(:merge_request) do
build(
:merge_request,
source_project: project,
source_branch: 'feature',
target_project: project,
target_branch: 'master'
)
end
before do
allow(project.repository).to receive(:code_owners_blob)
.with(ref: codeowner_lookup_ref)
.and_return(codeowner_blob)
end
context 'the MR contains a renamed file matching a file path' do expect(validate_code_owners).to eq(true)
let(:diff_check) { described_class.new(change_access) } end
let(:protected_branch) { build(:protected_branch, name: 'master', project: project) } end
before do context 'when updated from the web' do
expect(project).to receive(:branch_requires_code_owner_approval?) let(:protocol) { 'web' }
.at_least(:once).and_return(true)
# This particular commit renames a file: it 'returns false' do
allow(project.repository).to receive(:new_commits).and_return( expect(validate_code_owners).to eq(false)
[project.repository.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f')] end
) end
end end
it "returns an error message" do context 'when a user can push to the branch' do
expect { diff_check.validate! }.to raise_error do |error| let(:push_allowed) { true }
expect(error).to be_a(Gitlab::GitAccess::ForbiddenError)
expect(error.message).to include("CODEOWNERS` were matched:\n- *.js.coffee") it 'returns false' do
expect(validate_code_owners).to eq(false)
end end
end end
end end
context "the MR contains a matching file path" do describe "#validate_code_owners" do
let(:validation_result) do let!(:code_owner) { create(:user, username: "owner-1") }
subject.send(:validate_code_owners).call(["docs/CODEOWNERS", "README"]) let(:project) { create(:project, :repository) }
let(:codeowner_content) { "*.rb @#{code_owner.username}\ndocs/CODEOWNERS @owner-1\n*.js.coffee @owner-1" }
let(:codeowner_blob) { fake_blob(path: "CODEOWNERS", data: codeowner_content) }
let(:codeowner_blob_ref) { fake_blob(path: "CODEOWNERS", data: codeowner_content) }
let(:codeowner_lookup_ref) { merge_request.target_branch }
let(:merge_request) do
build(
:merge_request,
source_project: project,
source_branch: 'feature',
target_project: project,
target_branch: 'master'
)
end end
before do before do
expect(project).to receive(:branch_requires_code_owner_approval?) allow(project.repository).to receive(:code_owners_blob)
.at_least(:once).and_return(true) .with(ref: codeowner_lookup_ref)
.and_return(codeowner_blob)
end end
it_behaves_like "returns codeowners validation message" context 'the MR contains a renamed file matching a file path' do
end let(:diff_check) { described_class.new(change_access) }
let(:protected_branch) { build(:protected_branch, name: 'master', project: project) }
context "the MR doesn't contain a matching file path" do before do
it "returns nil" do expect(project).to receive(:branch_requires_code_owner_approval?)
expect(subject.send(:validate_code_owners) .at_least(:once).and_return(true)
.call(["docs/SAFE_FILE_NAME", "README"])).to be_nil
end
end
end
describe "#path_validations" do # This particular commit renames a file:
include_context 'change access checks context' allow(project.repository).to receive(:new_commits).and_return(
[project.repository.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f')]
)
end
context "when the feature isn't enabled on the project" do it "returns an error message" do
before do expect { diff_check.validate! }.to raise_error do |error|
expect(project).to receive(:branch_requires_code_owner_approval?) expect(error).to be_a(Gitlab::GitAccess::ForbiddenError)
.once.and_return(false) expect(error.message).to include("CODEOWNERS` were matched:\n- *.js.coffee")
end
end
end end
it "returns an empty array" do context "the MR contains a matching file path" do
expect(subject.send(:path_validations)).to eq([]) let(:validation_result) do
end subject.send(:validate_code_owners).call(["docs/CODEOWNERS", "README"])
end end
context "when the feature is enabled on the project" do
context "updated_from_web? == false" do
before do before do
expect(subject).to receive(:updated_from_web?).and_return(false)
expect(project).to receive(:branch_requires_code_owner_approval?) expect(project).to receive(:branch_requires_code_owner_approval?)
.once.and_return(true) .at_least(:once).and_return(true)
end end
it "returns an array of Proc(s)" do it_behaves_like "returns codeowners validation message"
validations = subject.send(:path_validations) end
expect(validations.any?).to be_truthy context "the MR doesn't contain a matching file path" do
expect(validations.any? { |v| !v.is_a? Proc }).to be_falsy it "returns nil" do
expect(subject.send(:validate_code_owners)
.call(["docs/SAFE_FILE_NAME", "README"])).to be_nil
end end
end end
end
describe "#path_validations" do
include_context 'change access checks context'
context "updated_from_web? == true" do context "when the feature isn't enabled on the project" do
before do before do
expect(subject).to receive(:updated_from_web?).and_return(true) expect(project).to receive(:branch_requires_code_owner_approval?)
.once.and_return(false)
end end
it "returns an empty array" do it "returns an empty array" do
expect(subject.send(:path_validations)).to eq([]) expect(subject.send(:path_validations)).to eq([])
end end
end end
end
end
context 'file name rules' do context "when the feature is enabled on the project" do
# Notice that the commit used creates a file named 'README' context "updated_from_web? == false" do
context 'file name regex check' do before do
let!(:push_rule) { create(:push_rule, file_name_regex: 'READ*') } expect(subject).to receive(:updated_from_web?).and_return(false)
expect(project).to receive(:branch_requires_code_owner_approval?)
.once.and_return(true)
end
it_behaves_like 'check ignored when push rule unlicensed' it "returns an array of Proc(s)" do
validations = subject.send(:path_validations)
it "returns an error if a new or renamed filed doesn't match the file name regex" do expect(validations.any?).to be_truthy
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "File name README was blacklisted by the pattern READ*.") expect(validations.any? { |v| !v.is_a? Proc }).to be_falsy
end end
end
it 'returns an error if the regex is invalid' do context "updated_from_web? == true" do
push_rule.file_name_regex = '+' before do
expect(subject).to receive(:updated_from_web?).and_return(true)
end
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /\ARegular expression '\+' is invalid/) it "returns an empty array" do
expect(subject.send(:path_validations)).to eq([])
end
end
end end
end end
context 'blacklisted files check' do context 'file name rules' do
let(:push_rule) { create(:push_rule, prevent_secrets: true) } # Notice that the commit used creates a file named 'README'
context 'file name regex check' do
let!(:push_rule) { create(:push_rule, file_name_regex: 'READ*') }
it_behaves_like 'check ignored when push rule unlicensed'
it "returns an error if a new or renamed filed doesn't match the file name regex" do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "File name README was blacklisted by the pattern READ*.")
end
it 'returns an error if the regex is invalid' do
push_rule.file_name_regex = '+'
it_behaves_like 'check ignored when push rule unlicensed' expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /\ARegular expression '\+' is invalid/)
end
end
it "returns true if there is no blacklisted files" do context 'blacklisted files check' do
new_rev = nil let(:push_rule) { create(:push_rule, prevent_secrets: true) }
white_listed = it_behaves_like 'check ignored when push rule unlicensed'
[
'readme.txt', 'any/ida_rsa.pub', 'any/id_dsa.pub', 'any_2/id_ed25519.pub', it "returns true if there is no blacklisted files" do
'random_file.pdf', 'folder/id_ecdsa.pub', 'docs/aws/credentials.md', 'ending_withhistory' new_rev = nil
white_listed =
[
'readme.txt', 'any/ida_rsa.pub', 'any/id_dsa.pub', 'any_2/id_ed25519.pub',
'random_file.pdf', 'folder/id_ecdsa.pub', 'docs/aws/credentials.md', 'ending_withhistory'
] ]
white_listed.each do |file_path| white_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c' old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev old_rev = new_rev if new_rev
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master") new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(project.repository).to receive(:new_commits).and_return( allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev) project.repository.commits_between(old_rev, new_rev)
) )
expect(subject.validate!).to be_truthy expect(subject.validate!).to be_truthy
end
end end
end
it "returns an error if a new or renamed filed doesn't match the file name regex" do it "returns an error if a new or renamed filed doesn't match the file name regex" do
new_rev = nil new_rev = nil
black_listed = black_listed =
[ [
'aws/credentials', '.ssh/personal_rsa', 'config/server_rsa', '.ssh/id_rsa', '.ssh/id_dsa', 'aws/credentials', '.ssh/personal_rsa', 'config/server_rsa', '.ssh/id_rsa', '.ssh/id_dsa',
'.ssh/personal_dsa', 'config/server_ed25519', 'any/id_ed25519', '.ssh/personal_ecdsa', 'config/server_ecdsa', '.ssh/personal_dsa', 'config/server_ed25519', 'any/id_ed25519', '.ssh/personal_ecdsa', 'config/server_ecdsa',
'any_place/id_ecdsa', 'some_pLace/file.key', 'other_PlAcE/other_file.pem', 'bye_bug.history', 'pg_sql_history' 'any_place/id_ecdsa', 'some_pLace/file.key', 'other_PlAcE/other_file.pem', 'bye_bug.history', 'pg_sql_history'
] ]
black_listed.each do |file_path| black_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c' old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
old_rev = new_rev if new_rev old_rev = new_rev if new_rev
new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master") new_rev = project.repository.create_file(user, file_path, "commit #{file_path}", message: "commit #{file_path}", branch_name: "master")
allow(subject).to receive(:commits).and_return( allow(subject).to receive(:commits).and_return(
project.repository.commits_between(old_rev, new_rev) project.repository.commits_between(old_rev, new_rev)
) )
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /File name #{file_path} was blacklisted by the pattern/) expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, /File name #{file_path} was blacklisted by the pattern/)
end
end end
end end
end end
end
context 'file lock rules' do
let_it_be(:push_rule) { create(:push_rule) }
let_it_be(:owner) { create(:user) }
let(:path_lock) { create(:path_lock, path: 'README', project: project) }
before do
project.add_developer(owner)
end
shared_examples 'a locked file' do context 'file lock rules' do
let!(:path_lock) { create(:path_lock, path: filename, project: project, user: owner) } let_it_be(:push_rule) { create(:push_rule) }
let_it_be(:owner) { create(:user) }
let(:path_lock) { create(:path_lock, path: 'README', project: project) }
before do before do
allow(project.repository).to receive(:new_commits).and_return( project.add_developer(owner)
[project.repository.commit(sha)]
)
end end
context 'and path is locked by another user' do shared_examples 'a locked file' do
it 'returns an error' do let!(:path_lock) { create(:path_lock, path: filename, project: project, user: owner) }
path_lock
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path '#{filename}' is locked by #{path_lock.user.name}") before do
allow(project.repository).to receive(:new_commits).and_return(
[project.repository.commit(sha)]
)
end end
end
context 'and path is locked by current user' do context 'and path is locked by another user' do
let(:user) { owner } it 'returns an error' do
path_lock
it 'is allows changes' do expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path '#{filename}' is locked by #{path_lock.user.name}")
path_lock end
end
expect { subject.validate! }.not_to raise_error context 'and path is locked by current user' do
let(:user) { owner }
it 'is allows changes' do
path_lock
expect { subject.validate! }.not_to raise_error
end
end end
end end
end
context 'when file has changes' do context 'when file has changes' do
let_it_be(:filename) { 'files/ruby/popen.rb' } let_it_be(:filename) { 'files/ruby/popen.rb' }
let_it_be(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' } let_it_be(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
it_behaves_like 'a locked file' it_behaves_like 'a locked file'
end end
context 'when file is renamed' do context 'when file is renamed' do
let_it_be(:filename) { 'files/js/commit.js.coffee' } let_it_be(:filename) { 'files/js/commit.js.coffee' }
let_it_be(:sha) { '6907208d755b60ebeacb2e9dfea74c92c3449a1f' } let_it_be(:sha) { '6907208d755b60ebeacb2e9dfea74c92c3449a1f' }
it_behaves_like 'a locked file' it_behaves_like 'a locked file'
end end
context 'when file is deleted' do context 'when file is deleted' do
let_it_be(:filename) { 'files/js/commit.js.coffee' } let_it_be(:filename) { 'files/js/commit.js.coffee' }
let_it_be(:sha) { 'd59c60028b053793cecfb4022de34602e1a9218e' } let_it_be(:sha) { 'd59c60028b053793cecfb4022de34602e1a9218e' }
it_behaves_like 'a locked file' it_behaves_like 'a locked file'
end end
it 'memoizes the validate_path_locks? call' do it 'memoizes the validate_path_locks? call' do
expect(project).to receive(:any_path_locks?).once.and_call_original expect(project).to receive(:any_path_locks?).once.and_call_original
2.times { subject.validate! } 2.times { subject.validate! }
end end
context 'when the branch is being deleted' do context 'when the branch is being deleted' do
let(:newrev) { Gitlab::Git::BLANK_SHA } let(:newrev) { Gitlab::Git::BLANK_SHA }
it 'does not run' do it 'does not run' do
path_lock path_lock
expect { subject.validate! }.not_to raise_error expect { subject.validate! }.not_to raise_error
end
end end
end
context 'when there is no valid change' do context 'when there is no valid change' do
let(:changes) { { oldrev: '_any', newrev: nil, ref: nil } } let(:changes) { { oldrev: '_any', newrev: nil, ref: nil } }
it 'does not run' do it 'does not run' do
path_lock path_lock
expect { subject.validate! }.not_to raise_error expect { subject.validate! }.not_to raise_error
end
end end
end end
end end
end end
it_behaves_like "diff check"
context 'when diff check with paths rpc feature flag is false' do
before do
stub_feature_flags(diff_check_with_paths_changed_rpc: false)
end
it_behaves_like "diff check"
end
end end
...@@ -17,17 +17,26 @@ module Gitlab ...@@ -17,17 +17,26 @@ module Gitlab
file_paths = [] file_paths = []
process_commits do |commit| if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project)
validate_once(commit) do paths = project.repository.find_changed_paths(commits.map(&:sha))
commit.raw_deltas.each do |diff| paths.each do |path|
file_paths.concat([diff.new_path, diff.old_path].compact) file_paths.concat([path.path])
validate_diff(diff) validate_diff(path)
end
else
process_commits do |commit|
validate_once(commit) do
commit.raw_deltas.each do |diff|
file_paths.concat([diff.new_path, diff.old_path].compact)
validate_diff(diff)
end
end end
end end
end end
validate_file_paths(file_paths) validate_file_paths(file_paths.uniq)
end end
private private
......
...@@ -467,6 +467,18 @@ module Gitlab ...@@ -467,6 +467,18 @@ module Gitlab
empty_diff_stats empty_diff_stats
end end
def find_changed_paths(commits)
processed_commits = commits.reject { |ref| ref.blank? || Gitlab::Git.blank_ref?(ref) }
return [] if processed_commits.empty?
wrapped_gitaly_errors do
gitaly_commit_client.find_changed_paths(processed_commits)
end
rescue CommandError, TypeError, NoRepository
[]
end
# Returns a RefName for a given SHA # Returns a RefName for a given SHA
def ref_name_for_sha(ref_path, sha) def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present? raise ArgumentError, "sha can't be empty" unless sha.present?
......
...@@ -216,6 +216,23 @@ module Gitlab ...@@ -216,6 +216,23 @@ module Gitlab
response.flat_map(&:stats) response.flat_map(&:stats)
end end
def find_changed_paths(commits)
request = Gitaly::FindChangedPathsRequest.new(
repository: @gitaly_repo,
commits: commits
)
response = GitalyClient.call(@repository.storage, :diff_service, :find_changed_paths, request, timeout: GitalyClient.medium_timeout)
response.flat_map do |msg|
msg.paths.map do |path|
OpenStruct.new(
status: path.status,
path: EncodingHelper.encode!(path.path)
)
end
end
end
def find_all_commits(opts = {}) def find_all_commits(opts = {})
request = Gitaly::FindAllCommitsRequest.new( request = Gitaly::FindAllCommitsRequest.new(
repository: @gitaly_repo, repository: @gitaly_repo,
......
...@@ -7,7 +7,6 @@ RSpec.describe Gitlab::Checks::DiffCheck do ...@@ -7,7 +7,6 @@ RSpec.describe Gitlab::Checks::DiffCheck do
describe '#validate!' do describe '#validate!' do
let(:owner) { create(:user) } let(:owner) { create(:user) }
let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do before do
allow(project.repository).to receive(:new_commits).and_return( allow(project.repository).to receive(:new_commits).and_return(
...@@ -28,13 +27,27 @@ RSpec.describe Gitlab::Checks::DiffCheck do ...@@ -28,13 +27,27 @@ RSpec.describe Gitlab::Checks::DiffCheck do
end end
context 'with LFS enabled' do context 'with LFS enabled' do
let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do before do
allow(project).to receive(:lfs_enabled?).and_return(true) allow(project).to receive(:lfs_enabled?).and_return(true)
end end
context 'when change is sent by a different user' do context 'when change is sent by a different user' do
it 'raises an error if the user is not allowed to update the file' do context 'when diff check with paths rpc feature flag is true' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}") it 'raises an error if the user is not allowed to update the file' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
end
end
context 'when diff check with paths rpc feature flag is false' do
before do
stub_feature_flags(diff_check_with_paths_changed_rpc: false)
end
it 'raises an error if the user is not allowed to update the file' do
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
end
end end
end end
...@@ -53,6 +66,8 @@ RSpec.describe Gitlab::Checks::DiffCheck do ...@@ -53,6 +66,8 @@ RSpec.describe Gitlab::Checks::DiffCheck do
expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original
stub_feature_flags(diff_check_with_paths_changed_rpc: false)
subject.validate! subject.validate!
end end
......
...@@ -1185,6 +1185,66 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do ...@@ -1185,6 +1185,66 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
end end
end end
describe '#find_changed_paths' do
let(:commit_1) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
let(:commit_2) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
let(:commit_3) { '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' }
let(:commit_1_files) do
[
OpenStruct.new(status: :ADDED, path: "files/executables/ls"),
OpenStruct.new(status: :ADDED, path: "files/executables/touch"),
OpenStruct.new(status: :ADDED, path: "files/links/regex.rb"),
OpenStruct.new(status: :ADDED, path: "files/links/ruby-style-guide.md"),
OpenStruct.new(status: :ADDED, path: "files/links/touch"),
OpenStruct.new(status: :MODIFIED, path: ".gitmodules"),
OpenStruct.new(status: :ADDED, path: "deeper/nested/six"),
OpenStruct.new(status: :ADDED, path: "nested/six")
]
end
let(:commit_2_files) do
[OpenStruct.new(status: :ADDED, path: "bin/executable")]
end
let(:commit_3_files) do
[
OpenStruct.new(status: :MODIFIED, path: ".gitmodules"),
OpenStruct.new(status: :ADDED, path: "gitlab-shell")
]
end
it 'returns a list of paths' do
collection = repository.find_changed_paths([commit_1, commit_2, commit_3])
expect(collection).to be_a(Enumerable)
expect(collection.to_a).to eq(commit_1_files + commit_2_files + commit_3_files)
end
it 'returns no paths when SHAs are invalid' do
collection = repository.find_changed_paths(['invalid', commit_1])
expect(collection).to be_a(Enumerable)
expect(collection.to_a).to be_empty
end
it 'returns a list of paths even when containing a blank ref' do
collection = repository.find_changed_paths([nil, commit_1])
expect(collection).to be_a(Enumerable)
expect(collection.to_a).to eq(commit_1_files)
end
it 'returns no paths when the commits are nil' do
expect_any_instance_of(Gitlab::GitalyClient::CommitService)
.not_to receive(:find_changed_paths)
collection = repository.find_changed_paths([nil, nil])
expect(collection).to be_a(Enumerable)
expect(collection.to_a).to be_empty
end
end
describe "#ls_files" do describe "#ls_files" do
let(:master_file_paths) { repository.ls_files("master") } let(:master_file_paths) { repository.ls_files("master") }
let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") } let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
......
...@@ -145,6 +145,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService do ...@@ -145,6 +145,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
end end
end end
describe '#find_changed_paths' do
let(:commits) { %w[1a0b36b3cdad1d2ee32457c102a8c0b7056fa863 cfe32cf61b73a0d5e9f13e774abde7ff789b1660] }
it 'sends an RPC request and returns the stats' do
request = Gitaly::FindChangedPathsRequest.new(repository: repository_message,
commits: commits)
changed_paths_response = Gitaly::FindChangedPathsResponse.new(
paths: [{
path: "app/assets/javascripts/boards/components/project_select.vue",
status: :MODIFIED
}])
expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:find_changed_paths)
.with(request, kind_of(Hash)).and_return([changed_paths_response])
returned_value = described_class.new(repository).find_changed_paths(commits)
mapped_returned_value = returned_value.map(&:to_h)
mapped_expected_value = changed_paths_response.paths.map(&:to_h)
expect(mapped_returned_value).to eq(mapped_expected_value)
end
end
describe '#tree_entries' do describe '#tree_entries' do
let(:path) { '/' } let(:path) { '/' }
......
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