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
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 13.6.1'
gem 'gitaly', '~> 13.7.0.pre.rc1'
gem 'grpc', '~> 1.30.2'
......
......@@ -422,7 +422,7 @@ GEM
rails (>= 3.2.0)
git (1.7.0)
rchardet (~> 1.8)
gitaly (13.6.1)
gitaly (13.7.0.pre.rc1)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-chronic (0.10.5)
......@@ -1349,7 +1349,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 13.6.1)
gitaly (~> 13.7.0.pre.rc1)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
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
def path_locks_validation
lambda do |diff|
path = if diff.renamed_file?
diff.old_path
path = if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project)
diff.path
else
diff.new_path || diff.old_path
if diff.renamed_file?
diff.old_path
else
diff.new_path || diff.old_path
end
end
lock_info = project.find_path_lock(path)
......@@ -72,12 +76,24 @@ module EE
end
end
def new_file?(path)
path.status == :ADDED
end
def file_name_validation
lambda do |diff|
if (diff.renamed_file || diff.new_file) && blacklisted_regex = push_rule.filename_denylisted?(diff.new_path)
return unless blacklisted_regex.present?
"File name #{diff.new_path} was blacklisted by the pattern #{blacklisted_regex}."
if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project)
if new_file?(diff) && denylisted_regex = push_rule.filename_denylisted?(diff.path)
return unless denylisted_regex.present?
"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
rescue ::PushRule::MatchError => e
raise ::Gitlab::GitAccess::ForbiddenError, e.message
......
......@@ -5,61 +5,37 @@ require 'spec_helper'
RSpec.describe Gitlab::Checks::DiffCheck do
include FakeBlobHelpers
include_context 'push rules checks context'
shared_examples_for "diff check" do
include_context 'push rules checks context'
describe '#validate!' do
let(:push_allowed) { false }
before do
allow(user_access).to receive(:can_push_to_branch?).and_return(push_allowed)
end
describe '#validate!' do
let(:push_allowed) { false }
shared_examples_for "returns codeowners validation message" do
it "returns an error message" do
expect(validation_result).to include("Pushes to protected branches")
before do
allow(user_access).to receive(:can_push_to_branch?).and_return(push_allowed)
end
end
context 'no push rules active' do
let_it_be(:push_rule) { create(:push_rule) }
it "does not attempt to check commits" do
expect(subject).not_to receive(:process_commits)
subject.validate!
shared_examples_for "returns codeowners validation message" do
it "returns an error message" do
expect(validation_result).to include("Pushes to protected branches")
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)
end
end
context 'no push rules active' do
let_it_be(:push_rule) { create(:push_rule) }
context 'when updated from the web' do
let(:protocol) { 'web' }
it "does not attempt to check commits" do
expect(subject).not_to receive(:process_commits)
it 'returns false' do
expect(validate_code_owners).to eq(false)
end
subject.validate!
end
end
context 'when a user can push to the branch' do
let(:push_allowed) { true }
it 'returns false' do
expect(validate_code_owners).to eq(false)
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 push_rules_supersede_code_owners is disabled' do
before do
......@@ -72,270 +48,306 @@ RSpec.describe Gitlab::Checks::DiffCheck do
expect(validate_code_owners).to eq(true)
end
end
end
end
describe "#validate_code_owners" do
let!(:code_owner) { create(:user, username: "owner-1") }
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
before do
allow(project.repository).to receive(:code_owners_blob)
.with(ref: codeowner_lookup_ref)
.and_return(codeowner_blob)
end
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)
context 'the MR contains a renamed file matching a file path' do
let(:diff_check) { described_class.new(change_access) }
let(:protected_branch) { build(:protected_branch, name: 'master', project: project) }
expect(validate_code_owners).to eq(true)
end
end
before do
expect(project).to receive(:branch_requires_code_owner_approval?)
.at_least(:once).and_return(true)
context 'when updated from the web' do
let(:protocol) { 'web' }
# This particular commit renames a file:
allow(project.repository).to receive(:new_commits).and_return(
[project.repository.commit('6907208d755b60ebeacb2e9dfea74c92c3449a1f')]
)
it 'returns false' do
expect(validate_code_owners).to eq(false)
end
end
end
it "returns an error message" do
expect { diff_check.validate! }.to raise_error do |error|
expect(error).to be_a(Gitlab::GitAccess::ForbiddenError)
expect(error.message).to include("CODEOWNERS` were matched:\n- *.js.coffee")
context 'when a user can push to the branch' do
let(:push_allowed) { true }
it 'returns false' do
expect(validate_code_owners).to eq(false)
end
end
end
context "the MR contains a matching file path" do
let(:validation_result) do
subject.send(:validate_code_owners).call(["docs/CODEOWNERS", "README"])
describe "#validate_code_owners" do
let!(:code_owner) { create(:user, username: "owner-1") }
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
before do
expect(project).to receive(:branch_requires_code_owner_approval?)
.at_least(:once).and_return(true)
allow(project.repository).to receive(:code_owners_blob)
.with(ref: codeowner_lookup_ref)
.and_return(codeowner_blob)
end
it_behaves_like "returns codeowners validation message"
end
context 'the MR contains a renamed file matching a file path' do
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
it "returns nil" do
expect(subject.send(:validate_code_owners)
.call(["docs/SAFE_FILE_NAME", "README"])).to be_nil
end
end
end
before do
expect(project).to receive(:branch_requires_code_owner_approval?)
.at_least(:once).and_return(true)
describe "#path_validations" do
include_context 'change access checks context'
# This particular commit renames a file:
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
before do
expect(project).to receive(:branch_requires_code_owner_approval?)
.once.and_return(false)
it "returns an error message" do
expect { diff_check.validate! }.to raise_error do |error|
expect(error).to be_a(Gitlab::GitAccess::ForbiddenError)
expect(error.message).to include("CODEOWNERS` were matched:\n- *.js.coffee")
end
end
end
it "returns an empty array" do
expect(subject.send(:path_validations)).to eq([])
end
end
context "the MR contains a matching file path" do
let(:validation_result) do
subject.send(:validate_code_owners).call(["docs/CODEOWNERS", "README"])
end
context "when the feature is enabled on the project" do
context "updated_from_web? == false" do
before do
expect(subject).to receive(:updated_from_web?).and_return(false)
expect(project).to receive(:branch_requires_code_owner_approval?)
.once.and_return(true)
.at_least(:once).and_return(true)
end
it "returns an array of Proc(s)" do
validations = subject.send(:path_validations)
it_behaves_like "returns codeowners validation message"
end
expect(validations.any?).to be_truthy
expect(validations.any? { |v| !v.is_a? Proc }).to be_falsy
context "the MR doesn't contain a matching file path" do
it "returns nil" do
expect(subject.send(:validate_code_owners)
.call(["docs/SAFE_FILE_NAME", "README"])).to be_nil
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
expect(subject).to receive(:updated_from_web?).and_return(true)
expect(project).to receive(:branch_requires_code_owner_approval?)
.once.and_return(false)
end
it "returns an empty array" do
expect(subject.send(:path_validations)).to eq([])
end
end
end
end
context 'file name rules' do
# 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*') }
context "when the feature is enabled on the project" do
context "updated_from_web? == false" do
before do
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 { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "File name README was blacklisted by the pattern READ*.")
end
expect(validations.any?).to be_truthy
expect(validations.any? { |v| !v.is_a? Proc }).to be_falsy
end
end
it 'returns an error if the regex is invalid' do
push_rule.file_name_regex = '+'
context "updated_from_web? == true" do
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
context 'blacklisted files check' do
let(:push_rule) { create(:push_rule, prevent_secrets: true) }
context 'file name rules' do
# 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
new_rev = nil
context 'blacklisted files check' do
let(:push_rule) { create(:push_rule, prevent_secrets: true) }
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'
it_behaves_like 'check ignored when push rule unlicensed'
it "returns true if there is no blacklisted files" do
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|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
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")
white_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
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")
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
allow(project.repository).to receive(:new_commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
expect(subject.validate!).to be_truthy
expect(subject.validate!).to be_truthy
end
end
end
it "returns an error if a new or renamed filed doesn't match the file name regex" do
new_rev = nil
it "returns an error if a new or renamed filed doesn't match the file name regex" do
new_rev = nil
black_listed =
[
'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',
'any_place/id_ecdsa', 'some_pLace/file.key', 'other_PlAcE/other_file.pem', 'bye_bug.history', 'pg_sql_history'
black_listed =
[
'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',
'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|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
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")
black_listed.each do |file_path|
old_rev = 'be93687618e4b132087f430a4d8fc3a609c9b77c'
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")
allow(subject).to receive(:commits).and_return(
project.repository.commits_between(old_rev, new_rev)
)
allow(subject).to receive(:commits).and_return(
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
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
let!(:path_lock) { create(:path_lock, path: filename, project: project, user: owner) }
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
allow(project.repository).to receive(:new_commits).and_return(
[project.repository.commit(sha)]
)
project.add_developer(owner)
end
context 'and path is locked by another user' do
it 'returns an error' do
path_lock
shared_examples 'a locked file' do
let!(:path_lock) { create(:path_lock, path: filename, project: project, user: owner) }
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
context 'and path is locked by current user' do
let(:user) { owner }
context 'and path is locked by another user' do
it 'returns an error' do
path_lock
it 'is allows changes' do
path_lock
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path '#{filename}' is locked by #{path_lock.user.name}")
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
context 'when file has changes' do
let_it_be(:filename) { 'files/ruby/popen.rb' }
let_it_be(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
context 'when file has changes' do
let_it_be(:filename) { 'files/ruby/popen.rb' }
let_it_be(:sha) { '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' }
it_behaves_like 'a locked file'
end
it_behaves_like 'a locked file'
end
context 'when file is renamed' do
let_it_be(:filename) { 'files/js/commit.js.coffee' }
let_it_be(:sha) { '6907208d755b60ebeacb2e9dfea74c92c3449a1f' }
context 'when file is renamed' do
let_it_be(:filename) { 'files/js/commit.js.coffee' }
let_it_be(:sha) { '6907208d755b60ebeacb2e9dfea74c92c3449a1f' }
it_behaves_like 'a locked file'
end
it_behaves_like 'a locked file'
end
context 'when file is deleted' do
let_it_be(:filename) { 'files/js/commit.js.coffee' }
let_it_be(:sha) { 'd59c60028b053793cecfb4022de34602e1a9218e' }
context 'when file is deleted' do
let_it_be(:filename) { 'files/js/commit.js.coffee' }
let_it_be(:sha) { 'd59c60028b053793cecfb4022de34602e1a9218e' }
it_behaves_like 'a locked file'
end
it_behaves_like 'a locked file'
end
it 'memoizes the validate_path_locks? call' do
expect(project).to receive(:any_path_locks?).once.and_call_original
it 'memoizes the validate_path_locks? call' do
expect(project).to receive(:any_path_locks?).once.and_call_original
2.times { subject.validate! }
end
2.times { subject.validate! }
end
context 'when the branch is being deleted' do
let(:newrev) { Gitlab::Git::BLANK_SHA }
context 'when the branch is being deleted' do
let(:newrev) { Gitlab::Git::BLANK_SHA }
it 'does not run' do
path_lock
it 'does not run' do
path_lock
expect { subject.validate! }.not_to raise_error
expect { subject.validate! }.not_to raise_error
end
end
end
context 'when there is no valid change' do
let(:changes) { { oldrev: '_any', newrev: nil, ref: nil } }
context 'when there is no valid change' do
let(:changes) { { oldrev: '_any', newrev: nil, ref: nil } }
it 'does not run' do
path_lock
it 'does not run' do
path_lock
expect { subject.validate! }.not_to raise_error
expect { subject.validate! }.not_to raise_error
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
......@@ -17,17 +17,26 @@ module Gitlab
file_paths = []
process_commits do |commit|
validate_once(commit) do
commit.raw_deltas.each do |diff|
file_paths.concat([diff.new_path, diff.old_path].compact)
if ::Feature.enabled?(:diff_check_with_paths_changed_rpc, project)
paths = project.repository.find_changed_paths(commits.map(&:sha))
paths.each do |path|
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
validate_file_paths(file_paths)
validate_file_paths(file_paths.uniq)
end
private
......
......@@ -467,6 +467,18 @@ module Gitlab
empty_diff_stats
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
def ref_name_for_sha(ref_path, sha)
raise ArgumentError, "sha can't be empty" unless sha.present?
......
......@@ -216,6 +216,23 @@ module Gitlab
response.flat_map(&:stats)
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 = {})
request = Gitaly::FindAllCommitsRequest.new(
repository: @gitaly_repo,
......
......@@ -7,7 +7,6 @@ RSpec.describe Gitlab::Checks::DiffCheck do
describe '#validate!' do
let(:owner) { create(:user) }
let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do
allow(project.repository).to receive(:new_commits).and_return(
......@@ -28,13 +27,27 @@ RSpec.describe Gitlab::Checks::DiffCheck do
end
context 'with LFS enabled' do
let!(:lock) { create(:lfs_file_lock, user: owner, project: project, path: 'README') }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
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
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, "The path 'README' is locked in Git LFS by #{lock.user.name}")
context 'when diff check with paths rpc feature flag is true' do
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
......@@ -53,6 +66,8 @@ RSpec.describe Gitlab::Checks::DiffCheck do
expect_any_instance_of(Commit).to receive(:raw_deltas).and_call_original
stub_feature_flags(diff_check_with_paths_changed_rpc: false)
subject.validate!
end
......
......@@ -1185,6 +1185,66 @@ RSpec.describe Gitlab::Git::Repository, :seed_helper do
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
let(:master_file_paths) { repository.ls_files("master") }
let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
......
......@@ -145,6 +145,31 @@ RSpec.describe Gitlab::GitalyClient::CommitService do
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
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