f1ae1e39ce
Moving the check out of the general requests, makes sure we don't have any slowdown in the regular requests. To keep the process performing this checks small, the check is still performed inside a unicorn. But that is called from a process running on the same server. Because the checks are now done outside normal request, we can have a simpler failure strategy: The check is now performed in the background every `circuitbreaker_check_interval`. Failures are logged in redis. The failures are reset when the check succeeds. Per check we will try `circuitbreaker_access_retries` times within `circuitbreaker_storage_timeout` seconds. When the number of failures exceeds `circuitbreaker_failure_count_threshold`, we will block access to the storage. After `failure_reset_time` of no checks, we will clear the stored failures. This could happen when the process that performs the checks is not running.
2346 lines
73 KiB
Ruby
2346 lines
73 KiB
Ruby
require 'spec_helper'
|
|
|
|
describe Repository do
|
|
include RepoHelpers
|
|
TestBlob = Struct.new(:path)
|
|
|
|
let(:project) { create(:project, :repository) }
|
|
let(:repository) { project.repository }
|
|
let(:broken_repository) { create(:project, :broken_storage).repository }
|
|
let(:user) { create(:user) }
|
|
let(:git_user) { Gitlab::Git::User.from_gitlab(user) }
|
|
|
|
let(:message) { 'Test message' }
|
|
|
|
let(:merge_commit) do
|
|
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
|
|
|
|
merge_commit_id = repository.merge(user,
|
|
merge_request.diff_head_sha,
|
|
merge_request,
|
|
message)
|
|
|
|
repository.commit(merge_commit_id)
|
|
end
|
|
|
|
let(:author_email) { 'user@example.org' }
|
|
let(:author_name) { 'John Doe' }
|
|
|
|
def expect_to_raise_storage_error
|
|
expect { yield }.to raise_error do |exception|
|
|
storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable]
|
|
known_exception = storage_exceptions.select { |e| exception.is_a?(e) }
|
|
|
|
expect(known_exception).not_to be_nil
|
|
end
|
|
end
|
|
|
|
describe '#branch_names_contains' do
|
|
subject { repository.branch_names_contains(sample_commit.id) }
|
|
|
|
it { is_expected.to include('master') }
|
|
it { is_expected.not_to include('feature') }
|
|
it { is_expected.not_to include('fix') }
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error do
|
|
broken_repository.branch_names_contains(sample_commit.id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#tag_names_contains' do
|
|
subject { repository.tag_names_contains(sample_commit.id) }
|
|
|
|
it { is_expected.to include('v1.1.0') }
|
|
it { is_expected.not_to include('v1.0.0') }
|
|
end
|
|
|
|
describe 'tags_sorted_by' do
|
|
context 'name' do
|
|
subject { repository.tags_sorted_by('name').map(&:name) }
|
|
|
|
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
|
|
end
|
|
|
|
context 'updated' do
|
|
let(:tag_a) { repository.find_tag('v1.0.0') }
|
|
let(:tag_b) { repository.find_tag('v1.1.0') }
|
|
|
|
context 'desc' do
|
|
subject { repository.tags_sorted_by('updated_desc').map(&:name) }
|
|
|
|
before do
|
|
double_first = double(committed_date: Time.now)
|
|
double_last = double(committed_date: Time.now - 1.second)
|
|
|
|
allow(tag_a).to receive(:dereferenced_target).and_return(double_first)
|
|
allow(tag_b).to receive(:dereferenced_target).and_return(double_last)
|
|
allow(repository).to receive(:tags).and_return([tag_a, tag_b])
|
|
end
|
|
|
|
it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
|
|
end
|
|
|
|
context 'asc' do
|
|
subject { repository.tags_sorted_by('updated_asc').map(&:name) }
|
|
|
|
before do
|
|
double_first = double(committed_date: Time.now - 1.second)
|
|
double_last = double(committed_date: Time.now)
|
|
|
|
allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
|
|
allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
|
|
allow(repository).to receive(:tags).and_return([tag_a, tag_b])
|
|
end
|
|
|
|
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
|
|
end
|
|
|
|
context 'annotated tag pointing to a blob' do
|
|
let(:annotated_tag_name) { 'annotated-tag' }
|
|
|
|
subject { repository.tags_sorted_by('updated_asc').map(&:name) }
|
|
|
|
before do
|
|
options = { message: 'test tag message\n',
|
|
tagger: { name: 'John Smith', email: 'john@gmail.com' } }
|
|
repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options)
|
|
|
|
double_first = double(committed_date: Time.now - 1.second)
|
|
double_last = double(committed_date: Time.now)
|
|
|
|
allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
|
|
allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
|
|
end
|
|
|
|
it { is_expected.to eq(['v1.1.0', 'v1.0.0', annotated_tag_name]) }
|
|
|
|
after do
|
|
repository.rugged.tags.delete(annotated_tag_name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#ref_name_for_sha' do
|
|
it 'returns the ref' do
|
|
allow(repository.raw_repository).to receive(:ref_name_for_sha)
|
|
.and_return('refs/environments/production/77')
|
|
|
|
expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
|
|
end
|
|
end
|
|
|
|
describe '#ref_exists?' do
|
|
context 'when ref exists' do
|
|
it 'returns true' do
|
|
expect(repository.ref_exists?('refs/heads/master')).to be true
|
|
end
|
|
end
|
|
|
|
context 'when ref does not exist' do
|
|
it 'returns false' do
|
|
expect(repository.ref_exists?('refs/heads/non-existent')).to be false
|
|
end
|
|
end
|
|
|
|
context 'when ref format is incorrect' do
|
|
it 'returns false' do
|
|
expect(repository.ref_exists?('refs/heads/invalid:master')).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#last_commit_for_path' do
|
|
shared_examples 'getting last commit for path' do
|
|
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
|
|
|
|
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error do
|
|
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly feature last_commit_for_path is enabled' do
|
|
it_behaves_like 'getting last commit for path'
|
|
end
|
|
|
|
context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
|
|
it_behaves_like 'getting last commit for path'
|
|
end
|
|
end
|
|
|
|
describe '#last_commit_id_for_path' do
|
|
shared_examples 'getting last commit ID for path' do
|
|
subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }
|
|
|
|
it "returns last commit id for a given path" do
|
|
is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
|
|
end
|
|
|
|
it "caches last commit id for a given path" do
|
|
cache = repository.send(:cache)
|
|
key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"
|
|
|
|
expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
|
|
is_expected.to eq('c1acaa5')
|
|
end
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error do
|
|
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly feature last_commit_for_path is enabled' do
|
|
it_behaves_like 'getting last commit ID for path'
|
|
end
|
|
|
|
context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
|
|
it_behaves_like 'getting last commit ID for path'
|
|
end
|
|
end
|
|
|
|
describe '#commits' do
|
|
it 'sets follow when path is a single path' do
|
|
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
|
|
|
|
repository.commits('master', path: 'README.md')
|
|
repository.commits('master', path: ['README.md'])
|
|
end
|
|
|
|
it 'does not set follow when path is multiple paths' do
|
|
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
|
|
|
|
repository.commits('master', path: ['README.md', 'CHANGELOG'])
|
|
end
|
|
|
|
it 'does not set follow when there are no paths' do
|
|
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
|
|
|
|
repository.commits('master')
|
|
end
|
|
end
|
|
|
|
describe '#find_commits_by_message' do
|
|
shared_examples 'finding commits by message' do
|
|
it 'returns commits with messages containing a given string' do
|
|
commit_ids = repository.find_commits_by_message('submodule').map(&:id)
|
|
|
|
expect(commit_ids).to include(
|
|
'5937ac0a7beb003549fc5fd26fc247adbce4a52e',
|
|
'6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
|
|
'cfe32cf61b73a0d5e9f13e774abde7ff789b1660'
|
|
)
|
|
expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
|
|
end
|
|
|
|
it 'is case insensitive' do
|
|
commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
|
|
|
|
expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly commits_by_message feature is enabled' do
|
|
it_behaves_like 'finding commits by message'
|
|
end
|
|
|
|
context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do
|
|
it_behaves_like 'finding commits by message'
|
|
end
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#blob_at' do
|
|
context 'blank sha' do
|
|
subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
describe '#merged_to_root_ref?' do
|
|
context 'merged branch without ff' do
|
|
subject { repository.merged_to_root_ref?('branch-merged') }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
# If the HEAD was ff then it will be false
|
|
context 'merged with ff' do
|
|
subject { repository.merged_to_root_ref?('improve/awesome') }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context 'not merged branch' do
|
|
subject { repository.merged_to_root_ref?('not-merged-branch') }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'default branch' do
|
|
subject { repository.merged_to_root_ref?('master') }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
end
|
|
|
|
describe '#can_be_merged?' do
|
|
context 'mergeable branches' do
|
|
subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }
|
|
|
|
it { is_expected.to be_truthy }
|
|
end
|
|
|
|
context 'non-mergeable branches' do
|
|
subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'non merged branch' do
|
|
subject { repository.merged_to_root_ref?('fix') }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'non existent branch' do
|
|
subject { repository.merged_to_root_ref?('non_existent_branch') }
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
describe '#commit' do
|
|
context 'when ref exists' do
|
|
it 'returns commit object' do
|
|
expect(repository.commit('master'))
|
|
.to be_an_instance_of Commit
|
|
end
|
|
end
|
|
|
|
context 'when ref does not exist' do
|
|
it 'returns nil' do
|
|
expect(repository.commit('non-existent-ref')).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when ref is not valid' do
|
|
context 'when preceding tree element exists' do
|
|
it 'returns nil' do
|
|
expect(repository.commit('master:ref')).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when preceding tree element does not exist' do
|
|
it 'returns nil' do
|
|
expect(repository.commit('non-existent:ref')).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#create_dir" do
|
|
it "commits a change that creates a new directory" do
|
|
expect do
|
|
repository.create_dir(user, 'newdir',
|
|
message: 'Create newdir', branch_name: 'master')
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
newdir = repository.tree('master', 'newdir')
|
|
expect(newdir.path).to eq('newdir')
|
|
end
|
|
|
|
context "when committing to another project" do
|
|
let(:forked_project) { create(:project, :repository) }
|
|
|
|
it "creates a fork and commit to the forked project" do
|
|
expect do
|
|
repository.create_dir(user, 'newdir',
|
|
message: 'Create newdir', branch_name: 'patch',
|
|
start_branch_name: 'master', start_project: forked_project)
|
|
end.to change { repository.commits('master').count }.by(0)
|
|
|
|
expect(repository.branch_exists?('patch')).to be_truthy
|
|
expect(forked_project.repository.branch_exists?('patch')).to be_falsy
|
|
|
|
newdir = repository.tree('patch', 'newdir')
|
|
expect(newdir.path).to eq('newdir')
|
|
end
|
|
end
|
|
|
|
context "when an author is specified" do
|
|
it "uses the given email/name to set the commit's author" do
|
|
expect do
|
|
repository.create_dir(user, 'newdir',
|
|
message: 'Add newdir',
|
|
branch_name: 'master',
|
|
author_email: author_email, author_name: author_name)
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
last_commit = repository.commit
|
|
|
|
expect(last_commit.author_email).to eq(author_email)
|
|
expect(last_commit.author_name).to eq(author_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#create_file" do
|
|
it 'commits new file successfully' do
|
|
expect do
|
|
repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
|
|
message: 'Create changelog',
|
|
branch_name: 'master')
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
blob = repository.blob_at('master', 'NEWCHANGELOG')
|
|
|
|
expect(blob.data).to eq('Changelog!')
|
|
end
|
|
|
|
it 'creates new file and dir when file_path has a forward slash' do
|
|
expect do
|
|
repository.create_file(user, 'new_dir/new_file.txt', 'File!',
|
|
message: 'Create new_file with new_dir',
|
|
branch_name: 'master')
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
expect(repository.tree('master', 'new_dir').path).to eq('new_dir')
|
|
expect(repository.blob_at('master', 'new_dir/new_file.txt').data).to eq('File!')
|
|
end
|
|
|
|
it 'respects the autocrlf setting' do
|
|
repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
|
|
message: 'Add hello world',
|
|
branch_name: 'master')
|
|
|
|
blob = repository.blob_at('master', 'hello.txt')
|
|
|
|
expect(blob.data).to eq("Hello,\nWorld")
|
|
end
|
|
|
|
context "when an author is specified" do
|
|
it "uses the given email/name to set the commit's author" do
|
|
expect do
|
|
repository.create_file(user, 'NEWREADME', 'README!',
|
|
message: 'Add README',
|
|
branch_name: 'master',
|
|
author_email: author_email,
|
|
author_name: author_name)
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
last_commit = repository.commit
|
|
|
|
expect(last_commit.author_email).to eq(author_email)
|
|
expect(last_commit.author_name).to eq(author_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#update_file" do
|
|
it 'updates file successfully' do
|
|
expect do
|
|
repository.update_file(user, 'CHANGELOG', 'Changelog!',
|
|
message: 'Update changelog',
|
|
branch_name: 'master')
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
blob = repository.blob_at('master', 'CHANGELOG')
|
|
|
|
expect(blob.data).to eq('Changelog!')
|
|
end
|
|
|
|
it 'updates filename successfully' do
|
|
expect do
|
|
repository.update_file(user, 'NEWLICENSE', 'Copyright!',
|
|
branch_name: 'master',
|
|
previous_path: 'LICENSE',
|
|
message: 'Changes filename')
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
files = repository.ls_files('master')
|
|
|
|
expect(files).not_to include('LICENSE')
|
|
expect(files).to include('NEWLICENSE')
|
|
end
|
|
|
|
context "when an author is specified" do
|
|
it "uses the given email/name to set the commit's author" do
|
|
expect do
|
|
repository.update_file(user, 'README', 'Updated README!',
|
|
branch_name: 'master',
|
|
previous_path: 'README',
|
|
message: 'Update README',
|
|
author_email: author_email,
|
|
author_name: author_name)
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
last_commit = repository.commit
|
|
|
|
expect(last_commit.author_email).to eq(author_email)
|
|
expect(last_commit.author_name).to eq(author_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#delete_file" do
|
|
it 'removes file successfully' do
|
|
expect do
|
|
repository.delete_file(user, 'README',
|
|
message: 'Remove README', branch_name: 'master')
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
expect(repository.blob_at('master', 'README')).to be_nil
|
|
end
|
|
|
|
context "when an author is specified" do
|
|
it "uses the given email/name to set the commit's author" do
|
|
expect do
|
|
repository.delete_file(user, 'README',
|
|
message: 'Remove README', branch_name: 'master',
|
|
author_email: author_email, author_name: author_name)
|
|
end.to change { repository.commits('master').count }.by(1)
|
|
|
|
last_commit = repository.commit
|
|
|
|
expect(last_commit.author_email).to eq(author_email)
|
|
expect(last_commit.author_name).to eq(author_name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#get_committer_and_author' do
|
|
it 'returns the committer and author data' do
|
|
options = repository.get_committer_and_author(user)
|
|
expect(options[:committer][:email]).to eq(user.email)
|
|
expect(options[:author][:email]).to eq(user.email)
|
|
end
|
|
|
|
context 'when the email/name are given' do
|
|
it 'returns an object containing the email/name' do
|
|
options = repository.get_committer_and_author(user, email: author_email, name: author_name)
|
|
expect(options[:author][:email]).to eq(author_email)
|
|
expect(options[:author][:name]).to eq(author_name)
|
|
end
|
|
end
|
|
|
|
context 'when the email is given but the name is not' do
|
|
it 'returns the committer as the author' do
|
|
options = repository.get_committer_and_author(user, email: author_email)
|
|
expect(options[:author][:email]).to eq(user.email)
|
|
expect(options[:author][:name]).to eq(user.name)
|
|
end
|
|
end
|
|
|
|
context 'when the name is given but the email is not' do
|
|
it 'returns nil' do
|
|
options = repository.get_committer_and_author(user, name: author_name)
|
|
expect(options[:author][:email]).to eq(user.email)
|
|
expect(options[:author][:name]).to eq(user.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "search_files_by_content" do
|
|
let(:results) { repository.search_files_by_content('feature', 'master') }
|
|
subject { results }
|
|
|
|
it { is_expected.to be_an Array }
|
|
|
|
it 'regex-escapes the query string' do
|
|
results = repository.search_files_by_content("test\\", 'master')
|
|
|
|
expect(results.first).not_to start_with('fatal:')
|
|
end
|
|
|
|
it 'properly handles an unmatched parenthesis' do
|
|
results = repository.search_files_by_content("test(", 'master')
|
|
|
|
expect(results.first).not_to start_with('fatal:')
|
|
end
|
|
|
|
it 'properly handles when query is not present' do
|
|
results = repository.search_files_by_content('', 'master')
|
|
|
|
expect(results).to match_array([])
|
|
end
|
|
|
|
it 'properly handles query when repo is empty' do
|
|
repository = create(:project, :empty_repo).repository
|
|
results = repository.search_files_by_content('test', 'master')
|
|
|
|
expect(results).to match_array([])
|
|
end
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error do
|
|
broken_repository.search_files_by_content('feature', 'master')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'result' do
|
|
subject { results.first }
|
|
|
|
it { is_expected.to be_an String }
|
|
it { expect(subject.lines[2]).to eq("master:CHANGELOG:190: - Feature: Replace teams with group membership\n") }
|
|
end
|
|
end
|
|
|
|
describe "search_files_by_name" do
|
|
let(:results) { repository.search_files_by_name('files', 'master') }
|
|
|
|
it 'returns result' do
|
|
expect(results.first).to eq('files/html/500.html')
|
|
end
|
|
|
|
it 'properly handles when query is not present' do
|
|
results = repository.search_files_by_name('', 'master')
|
|
|
|
expect(results).to match_array([])
|
|
end
|
|
|
|
it 'properly handles query when repo is empty' do
|
|
repository = create(:project, :empty_repo).repository
|
|
|
|
results = repository.search_files_by_name('test', 'master')
|
|
|
|
expect(results).to match_array([])
|
|
end
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#fetch_ref' do
|
|
let(:broken_repository) { create(:project, :broken_storage).repository }
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error do
|
|
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#create_ref' do
|
|
it 'redirects the call to write_ref' do
|
|
ref, ref_path = '1', '2'
|
|
|
|
expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref)
|
|
|
|
repository.create_ref(ref, ref_path)
|
|
end
|
|
end
|
|
|
|
describe "#changelog", :use_clean_rails_memory_store_caching do
|
|
it 'accepts changelog' do
|
|
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])
|
|
|
|
expect(repository.changelog.path).to eq('changelog')
|
|
end
|
|
|
|
it 'accepts news instead of changelog' do
|
|
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')])
|
|
|
|
expect(repository.changelog.path).to eq('news')
|
|
end
|
|
|
|
it 'accepts history instead of changelog' do
|
|
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')])
|
|
|
|
expect(repository.changelog.path).to eq('history')
|
|
end
|
|
|
|
it 'accepts changes instead of changelog' do
|
|
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')])
|
|
|
|
expect(repository.changelog.path).to eq('changes')
|
|
end
|
|
|
|
it 'is case-insensitive' do
|
|
expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')])
|
|
|
|
expect(repository.changelog.path).to eq('CHANGELOG')
|
|
end
|
|
end
|
|
|
|
describe "#license_blob", :use_clean_rails_memory_store_caching do
|
|
before do
|
|
repository.delete_file(
|
|
user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
|
|
end
|
|
|
|
it 'handles when HEAD points to non-existent ref' do
|
|
repository.create_file(
|
|
user, 'LICENSE', 'Copyright!',
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
allow(repository).to receive(:file_on_head)
|
|
.and_raise(Rugged::ReferenceError)
|
|
|
|
expect(repository.license_blob).to be_nil
|
|
end
|
|
|
|
it 'looks in the root_ref only' do
|
|
repository.delete_file(user, 'LICENSE',
|
|
message: 'Remove LICENSE', branch_name: 'markdown')
|
|
repository.create_file(user, 'LICENSE',
|
|
Licensee::License.new('mit').content,
|
|
message: 'Add LICENSE', branch_name: 'markdown')
|
|
|
|
expect(repository.license_blob).to be_nil
|
|
end
|
|
|
|
it 'detects license file with no recognizable open-source license content' do
|
|
repository.create_file(user, 'LICENSE', 'Copyright!',
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
expect(repository.license_blob.path).to eq('LICENSE')
|
|
end
|
|
|
|
%w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
|
|
it "detects '#{filename}'" do
|
|
repository.create_file(user, filename,
|
|
Licensee::License.new('mit').content,
|
|
message: "Add #{filename}", branch_name: 'master')
|
|
|
|
expect(repository.license_blob.name).to eq(filename)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#license_key', :use_clean_rails_memory_store_caching do
|
|
before do
|
|
repository.delete_file(user, 'LICENSE',
|
|
message: 'Remove LICENSE', branch_name: 'master')
|
|
end
|
|
|
|
it 'returns nil when no license is detected' do
|
|
expect(repository.license_key).to be_nil
|
|
end
|
|
|
|
it 'returns nil when the repository does not exist' do
|
|
expect(repository).to receive(:exists?).and_return(false)
|
|
|
|
expect(repository.license_key).to be_nil
|
|
end
|
|
|
|
it 'returns nil when the content is not recognizable' do
|
|
repository.create_file(user, 'LICENSE', 'Copyright!',
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
expect(repository.license_key).to be_nil
|
|
end
|
|
|
|
it 'returns the license key' do
|
|
repository.create_file(user, 'LICENSE',
|
|
Licensee::License.new('mit').content,
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
expect(repository.license_key).to eq('mit')
|
|
end
|
|
end
|
|
|
|
describe '#license' do
|
|
before do
|
|
repository.delete_file(user, 'LICENSE',
|
|
message: 'Remove LICENSE', branch_name: 'master')
|
|
end
|
|
|
|
it 'returns nil when no license is detected' do
|
|
expect(repository.license).to be_nil
|
|
end
|
|
|
|
it 'returns nil when the repository does not exist' do
|
|
expect(repository).to receive(:exists?).and_return(false)
|
|
|
|
expect(repository.license).to be_nil
|
|
end
|
|
|
|
it 'returns nil when the content is not recognizable' do
|
|
repository.create_file(user, 'LICENSE', 'Copyright!',
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
expect(repository.license).to be_nil
|
|
end
|
|
|
|
it 'returns the license' do
|
|
license = Licensee::License.new('mit')
|
|
repository.create_file(user, 'LICENSE',
|
|
license.content,
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
expect(repository.license).to eq(license)
|
|
end
|
|
end
|
|
|
|
describe "#gitlab_ci_yml", :use_clean_rails_memory_store_caching do
|
|
it 'returns valid file' do
|
|
files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
|
|
expect(repository.tree).to receive(:blobs).and_return(files)
|
|
|
|
expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml')
|
|
end
|
|
|
|
it 'returns nil if not exists' do
|
|
expect(repository.tree).to receive(:blobs).and_return([])
|
|
expect(repository.gitlab_ci_yml).to be_nil
|
|
end
|
|
|
|
it 'returns nil for empty repository' do
|
|
allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError)
|
|
expect(repository.gitlab_ci_yml).to be_nil
|
|
end
|
|
end
|
|
|
|
describe '#add_branch' do
|
|
let(:branch_name) { 'new_feature' }
|
|
let(:target) { 'master' }
|
|
|
|
subject { repository.add_branch(user, branch_name, target) }
|
|
|
|
context 'with Gitaly enabled' do
|
|
it "calls Gitaly's OperationService" do
|
|
expect_any_instance_of(Gitlab::GitalyClient::OperationService)
|
|
.to receive(:user_create_branch).with(branch_name, user, target)
|
|
.and_return(nil)
|
|
|
|
subject
|
|
end
|
|
|
|
it 'creates_the_branch' do
|
|
expect(subject.name).to eq(branch_name)
|
|
expect(repository.find_branch(branch_name)).not_to be_nil
|
|
end
|
|
|
|
context 'with a non-existing target' do
|
|
let(:target) { 'fake-target' }
|
|
|
|
it "returns false and doesn't create the branch" do
|
|
expect(subject).to be(false)
|
|
expect(repository.find_branch(branch_name)).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with Gitaly disabled', :skip_gitaly_mock do
|
|
context 'when pre hooks were successful' do
|
|
it 'runs without errors' do
|
|
hook = double(trigger: [true, nil])
|
|
expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)
|
|
|
|
expect { subject }.not_to raise_error
|
|
end
|
|
|
|
it 'creates the branch' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
|
|
|
|
expect(subject.name).to eq(branch_name)
|
|
end
|
|
|
|
it 'calls the after_create_branch hook' do
|
|
expect(repository).to receive(:after_create_branch)
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
context 'when pre hooks failed' do
|
|
it 'gets an error' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
|
|
|
|
expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
|
|
end
|
|
|
|
it 'does not create the branch' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
|
|
|
|
expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
|
|
expect(repository.find_branch(branch_name)).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#find_branch' do
|
|
it 'loads a branch with a fresh repo' do
|
|
expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original
|
|
|
|
2.times do
|
|
expect(repository.find_branch('feature')).not_to be_nil
|
|
end
|
|
end
|
|
|
|
it 'loads a branch with a cached repo' do
|
|
expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original
|
|
|
|
2.times do
|
|
expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#update_branch_with_hooks' do
|
|
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
|
|
let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
|
|
let(:updating_ref) { 'refs/heads/feature' }
|
|
let(:target_project) { project }
|
|
let(:target_repository) { target_project.repository }
|
|
|
|
context 'when pre hooks were successful' do
|
|
before do
|
|
service = Gitlab::Git::HooksService.new
|
|
expect(Gitlab::Git::HooksService).to receive(:new).and_return(service)
|
|
expect(service).to receive(:execute)
|
|
.with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref)
|
|
.and_yield(service).and_return(true)
|
|
end
|
|
|
|
it 'runs without errors' do
|
|
expect do
|
|
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
|
|
new_rev
|
|
end
|
|
end.not_to raise_error
|
|
end
|
|
|
|
it 'ensures the autocrlf Git option is set to :input' do
|
|
service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository)
|
|
|
|
expect(service).to receive(:update_autocrlf_option)
|
|
|
|
service.with_branch('feature') { new_rev }
|
|
end
|
|
|
|
context "when the branch wasn't empty" do
|
|
it 'updates the head' do
|
|
expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
|
|
|
|
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
|
|
new_rev
|
|
end
|
|
|
|
expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
|
|
end
|
|
end
|
|
|
|
context 'when target project does not have the commit' do
|
|
let(:target_project) { create(:project, :empty_repo) }
|
|
let(:old_rev) { Gitlab::Git::BLANK_SHA }
|
|
let(:new_rev) { project.commit('feature').sha }
|
|
let(:updating_ref) { 'refs/heads/master' }
|
|
|
|
it 'fetch_ref and create the branch' do
|
|
expect(target_project.repository.raw_repository).to receive(:fetch_ref)
|
|
.and_call_original
|
|
|
|
Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
|
|
.with_branch(
|
|
'master',
|
|
start_repository: project.repository.raw_repository,
|
|
start_branch_name: 'feature') { new_rev }
|
|
|
|
expect(target_repository.branch_names).to contain_exactly('master')
|
|
end
|
|
end
|
|
|
|
context 'when target project already has the commit' do
|
|
let(:target_project) { create(:project, :repository) }
|
|
|
|
it 'does not fetch_ref and just pass the commit' do
|
|
expect(target_repository).not_to receive(:fetch_ref)
|
|
|
|
Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
|
|
.with_branch('feature', start_repository: project.repository.raw_repository) { new_rev }
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when temporary ref failed to be created from other project' do
|
|
let(:target_project) { create(:project, :empty_repo) }
|
|
|
|
before do
|
|
expect(target_project.repository.raw_repository).to receive(:run_git)
|
|
end
|
|
|
|
it 'raises Rugged::ReferenceError' do
|
|
raise_reference_error = raise_error(Rugged::ReferenceError) do |err|
|
|
expect(err.cause).to be_nil
|
|
end
|
|
|
|
expect do
|
|
Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository)
|
|
.with_branch('feature',
|
|
start_repository: project.repository.raw_repository,
|
|
&:itself)
|
|
end.to raise_reference_error
|
|
end
|
|
end
|
|
|
|
context 'when the update adds more than one commit' do
|
|
let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' }
|
|
|
|
it 'runs without errors' do
|
|
# old_rev is an ancestor of new_rev
|
|
expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
|
|
|
|
# old_rev is not a direct ancestor (parent) of new_rev
|
|
expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
|
|
|
|
branch = 'feature-ff-target'
|
|
repository.add_branch(user, branch, old_rev)
|
|
|
|
expect do
|
|
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
|
|
new_rev
|
|
end
|
|
end.not_to raise_error
|
|
end
|
|
end
|
|
|
|
context 'when the update would remove commits from the target branch' do
|
|
let(:branch) { 'master' }
|
|
let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha }
|
|
|
|
it 'raises an exception' do
|
|
# The 'master' branch is NOT an ancestor of new_rev.
|
|
expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
|
|
|
|
# Updating 'master' to new_rev would lose the commits on 'master' that
|
|
# are not contained in new_rev. This should not be allowed.
|
|
expect do
|
|
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
|
|
new_rev
|
|
end
|
|
end.to raise_error(Gitlab::Git::CommitError)
|
|
end
|
|
end
|
|
|
|
context 'when pre hooks failed' do
|
|
it 'gets an error' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
|
|
|
|
expect do
|
|
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
|
|
new_rev
|
|
end
|
|
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
|
|
end
|
|
end
|
|
|
|
context 'when target branch is different from source branch' do
|
|
before do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
|
|
end
|
|
|
|
it 'expires branch cache' do
|
|
expect(repository).not_to receive(:expire_exists_cache)
|
|
expect(repository).not_to receive(:expire_root_ref_cache)
|
|
expect(repository).not_to receive(:expire_emptiness_caches)
|
|
expect(repository).to receive(:expire_branches_cache)
|
|
|
|
repository.with_branch(user, 'new-feature') do
|
|
new_rev
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when repository is empty' do
|
|
before do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
|
|
end
|
|
|
|
it 'expires creation and branch cache' do
|
|
empty_repository = create(:project, :empty_repo).repository
|
|
|
|
expect(empty_repository).to receive(:expire_exists_cache)
|
|
expect(empty_repository).to receive(:expire_root_ref_cache)
|
|
expect(empty_repository).to receive(:expire_emptiness_caches)
|
|
expect(empty_repository).to receive(:expire_branches_cache)
|
|
|
|
empty_repository.create_file(user, 'CHANGELOG', 'Changelog!',
|
|
message: 'Updates file content',
|
|
branch_name: 'master')
|
|
end
|
|
end
|
|
end
|
|
|
|
shared_examples 'repo exists check' do
|
|
it 'returns true when a repository exists' do
|
|
expect(repository.exists?).to eq(true)
|
|
end
|
|
|
|
it 'returns false if no full path can be constructed' do
|
|
allow(repository).to receive(:full_path).and_return(nil)
|
|
|
|
expect(repository.exists?).to eq(false)
|
|
end
|
|
|
|
context 'with broken storage', :broken_storage do
|
|
it 'should raise a storage error' do
|
|
expect_to_raise_storage_error { broken_repository.exists? }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#exists?' do
|
|
context 'when repository_exists is disabled' do
|
|
it_behaves_like 'repo exists check'
|
|
end
|
|
|
|
context 'when repository_exists is enabled', :skip_gitaly_mock do
|
|
it_behaves_like 'repo exists check'
|
|
end
|
|
end
|
|
|
|
describe '#has_visible_content?' do
|
|
before do
|
|
# If raw_repository.has_visible_content? gets called more than once then
|
|
# caching is broken. We don't want that.
|
|
expect(repository.raw_repository).to receive(:has_visible_content?)
|
|
.once
|
|
.and_return(result)
|
|
end
|
|
|
|
context 'when true' do
|
|
let(:result) { true }
|
|
|
|
it 'returns true and caches it' do
|
|
expect(repository.has_visible_content?).to eq(true)
|
|
# Second call hits the cache
|
|
expect(repository.has_visible_content?).to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'when false' do
|
|
let(:result) { false }
|
|
|
|
it 'returns false and caches it' do
|
|
expect(repository.has_visible_content?).to eq(false)
|
|
# Second call hits the cache
|
|
expect(repository.has_visible_content?).to eq(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#branch_exists?' do
|
|
it 'uses branch_names' do
|
|
allow(repository).to receive(:branch_names).and_return(['foobar'])
|
|
|
|
expect(repository.branch_exists?('foobar')).to eq(true)
|
|
expect(repository.branch_exists?('master')).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe '#branch_names', :use_clean_rails_memory_store_caching do
|
|
let(:fake_branch_names) { ['foobar'] }
|
|
|
|
it 'gets cached across Repository instances' do
|
|
allow(repository.raw_repository).to receive(:branch_names).once.and_return(fake_branch_names)
|
|
|
|
expect(repository.branch_names).to eq(fake_branch_names)
|
|
|
|
fresh_repository = Project.find(project.id).repository
|
|
expect(fresh_repository.object_id).not_to eq(repository.object_id)
|
|
|
|
expect(fresh_repository.raw_repository).not_to receive(:branch_names)
|
|
expect(fresh_repository.branch_names).to eq(fake_branch_names)
|
|
end
|
|
end
|
|
|
|
describe '#update_autocrlf_option' do
|
|
describe 'when autocrlf is not already set to :input' do
|
|
before do
|
|
repository.raw_repository.autocrlf = true
|
|
end
|
|
|
|
it 'sets autocrlf to :input' do
|
|
Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option)
|
|
|
|
expect(repository.raw_repository.autocrlf).to eq(:input)
|
|
end
|
|
end
|
|
|
|
describe 'when autocrlf is already set to :input' do
|
|
before do
|
|
repository.raw_repository.autocrlf = :input
|
|
end
|
|
|
|
it 'does nothing' do
|
|
expect(repository.raw_repository).not_to receive(:autocrlf=)
|
|
.with(:input)
|
|
|
|
Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#empty?' do
|
|
let(:empty_repository) { create(:project_empty_repo).repository }
|
|
|
|
it 'returns true for an empty repository' do
|
|
expect(empty_repository).to be_empty
|
|
end
|
|
|
|
it 'returns false for a non-empty repository' do
|
|
expect(repository).not_to be_empty
|
|
end
|
|
|
|
it 'caches the output' do
|
|
expect(repository.raw_repository).to receive(:has_visible_content?).once
|
|
|
|
repository.empty?
|
|
repository.empty?
|
|
end
|
|
end
|
|
|
|
describe '#root_ref' do
|
|
it 'returns a branch name' do
|
|
expect(repository.root_ref).to be_an_instance_of(String)
|
|
end
|
|
|
|
it 'caches the output' do
|
|
expect(repository.raw_repository).to receive(:root_ref)
|
|
.once
|
|
.and_return('master')
|
|
|
|
repository.root_ref
|
|
repository.root_ref
|
|
end
|
|
end
|
|
|
|
describe '#expire_root_ref_cache' do
|
|
it 'expires the root reference cache' do
|
|
repository.root_ref
|
|
|
|
expect(repository.raw_repository).to receive(:root_ref)
|
|
.once
|
|
.and_return('foo')
|
|
|
|
repository.expire_root_ref_cache
|
|
|
|
expect(repository.root_ref).to eq('foo')
|
|
end
|
|
end
|
|
|
|
describe '#expire_branch_cache' do
|
|
# This method is private but we need it for testing purposes. Sadly there's
|
|
# no other proper way of testing caching operations.
|
|
let(:cache) { repository.send(:cache) }
|
|
|
|
it 'expires the cache for all branches' do
|
|
expect(cache).to receive(:expire)
|
|
.at_least(repository.branches.length * 2)
|
|
.times
|
|
|
|
repository.expire_branch_cache
|
|
end
|
|
|
|
it 'expires the cache for all branches when the root branch is given' do
|
|
expect(cache).to receive(:expire)
|
|
.at_least(repository.branches.length * 2)
|
|
.times
|
|
|
|
repository.expire_branch_cache(repository.root_ref)
|
|
end
|
|
|
|
it 'expires the cache for a specific branch' do
|
|
expect(cache).to receive(:expire).twice
|
|
|
|
repository.expire_branch_cache('foo')
|
|
end
|
|
end
|
|
|
|
describe '#expire_emptiness_caches' do
|
|
let(:cache) { repository.send(:cache) }
|
|
|
|
it 'expires the caches for an empty repository' do
|
|
allow(repository).to receive(:empty?).and_return(true)
|
|
|
|
expect(cache).to receive(:expire).with(:empty?)
|
|
expect(cache).to receive(:expire).with(:has_visible_content?)
|
|
|
|
repository.expire_emptiness_caches
|
|
end
|
|
|
|
it 'does not expire the cache for a non-empty repository' do
|
|
allow(repository).to receive(:empty?).and_return(false)
|
|
|
|
expect(cache).not_to receive(:expire).with(:empty?)
|
|
expect(cache).not_to receive(:expire).with(:has_visible_content?)
|
|
|
|
repository.expire_emptiness_caches
|
|
end
|
|
end
|
|
|
|
describe 'skip_merges option' do
|
|
subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map { |k| k.id } }
|
|
|
|
it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
|
|
end
|
|
|
|
describe '#merge' do
|
|
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
|
|
|
|
let(:message) { 'Test \r\n\r\n message' }
|
|
|
|
shared_examples '#merge' do
|
|
it 'merges the code and returns the commit id' do
|
|
expect(merge_commit).to be_present
|
|
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
|
|
end
|
|
|
|
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
|
|
merge_commit_id = merge(repository, user, merge_request, message)
|
|
|
|
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
|
|
end
|
|
|
|
it 'removes carriage returns from commit message' do
|
|
merge_commit_id = merge(repository, user, merge_request, message)
|
|
|
|
expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
|
|
end
|
|
end
|
|
|
|
context 'with gitaly' do
|
|
it_behaves_like '#merge'
|
|
end
|
|
|
|
context 'without gitaly', :skip_gitaly_mock do
|
|
it_behaves_like '#merge'
|
|
end
|
|
|
|
def merge(repository, user, merge_request, message)
|
|
repository.merge(user, merge_request.diff_head_sha, merge_request, message)
|
|
end
|
|
end
|
|
|
|
describe '#ff_merge' do
|
|
before do
|
|
repository.add_branch(user, 'ff-target', 'feature~5')
|
|
end
|
|
|
|
it 'merges the code and return the commit id' do
|
|
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
|
|
merge_commit_id = repository.ff_merge(user,
|
|
merge_request.diff_head_sha,
|
|
merge_request.target_branch,
|
|
merge_request: merge_request)
|
|
merge_commit = repository.commit(merge_commit_id)
|
|
|
|
expect(merge_commit).to be_present
|
|
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
|
|
end
|
|
|
|
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
|
|
merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
|
|
merge_commit_id = repository.ff_merge(user,
|
|
merge_request.diff_head_sha,
|
|
merge_request.target_branch,
|
|
merge_request: merge_request)
|
|
|
|
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
|
|
end
|
|
end
|
|
|
|
describe '#revert' do
|
|
shared_examples 'reverting a commit' do
|
|
let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
|
|
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
|
|
let(:message) { 'revert message' }
|
|
|
|
context 'when there is a conflict' do
|
|
it 'raises an error' do
|
|
expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
|
|
end
|
|
end
|
|
|
|
context 'when commit was already reverted' do
|
|
it 'raises an error' do
|
|
repository.revert(user, update_image_commit, 'master', message)
|
|
|
|
expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
|
|
end
|
|
end
|
|
|
|
context 'when commit can be reverted' do
|
|
it 'reverts the changes' do
|
|
expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'reverting a merge commit' do
|
|
it 'reverts the changes' do
|
|
merge_commit
|
|
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present
|
|
|
|
repository.revert(user, merge_commit, 'master', message)
|
|
expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly revert feature is enabled' do
|
|
it_behaves_like 'reverting a commit'
|
|
end
|
|
|
|
context 'when Gitaly revert feature is disabled', :disable_gitaly do
|
|
it_behaves_like 'reverting a commit'
|
|
end
|
|
end
|
|
|
|
describe '#cherry_pick' do
|
|
shared_examples 'cherry-picking a commit' do
|
|
let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
|
|
let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
|
|
let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
|
|
let(:message) { 'cherry-pick message' }
|
|
|
|
context 'when there is a conflict' do
|
|
it 'raises an error' do
|
|
expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
|
|
end
|
|
end
|
|
|
|
context 'when commit was already cherry-picked' do
|
|
it 'raises an error' do
|
|
repository.cherry_pick(user, pickable_commit, 'master', message)
|
|
|
|
expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
|
|
end
|
|
end
|
|
|
|
context 'when commit can be cherry-picked' do
|
|
it 'cherry-picks the changes' do
|
|
expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'cherry-picking a merge commit' do
|
|
it 'cherry-picks the changes' do
|
|
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil
|
|
|
|
cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
|
|
cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message
|
|
|
|
expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
|
|
expect(cherry_pick_commit_message).to eq(message)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly cherry_pick feature is enabled' do
|
|
it_behaves_like 'cherry-picking a commit'
|
|
end
|
|
|
|
context 'when Gitaly cherry_pick feature is disabled', :disable_gitaly do
|
|
it_behaves_like 'cherry-picking a commit'
|
|
end
|
|
end
|
|
|
|
describe '#before_delete' do
|
|
describe 'when a repository does not exist' do
|
|
before do
|
|
allow(repository).to receive(:exists?).and_return(false)
|
|
end
|
|
|
|
it 'does not flush caches that depend on repository data' do
|
|
expect(repository).not_to receive(:expire_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the tags cache' do
|
|
expect(repository).to receive(:expire_tags_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the branches cache' do
|
|
expect(repository).to receive(:expire_branches_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the root ref cache' do
|
|
expect(repository).to receive(:expire_root_ref_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the emptiness caches' do
|
|
expect(repository).to receive(:expire_emptiness_caches)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the exists cache' do
|
|
expect(repository).to receive(:expire_exists_cache).twice
|
|
|
|
repository.before_delete
|
|
end
|
|
end
|
|
|
|
describe 'when a repository exists' do
|
|
before do
|
|
allow(repository).to receive(:exists?).and_return(true)
|
|
end
|
|
|
|
it 'flushes the tags cache' do
|
|
expect(repository).to receive(:expire_tags_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the branches cache' do
|
|
expect(repository).to receive(:expire_branches_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the root ref cache' do
|
|
expect(repository).to receive(:expire_root_ref_cache)
|
|
|
|
repository.before_delete
|
|
end
|
|
|
|
it 'flushes the emptiness caches' do
|
|
expect(repository).to receive(:expire_emptiness_caches)
|
|
|
|
repository.before_delete
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#before_change_head' do
|
|
it 'flushes the branch cache' do
|
|
expect(repository).to receive(:expire_branch_cache)
|
|
|
|
repository.before_change_head
|
|
end
|
|
|
|
it 'flushes the root ref cache' do
|
|
expect(repository).to receive(:expire_root_ref_cache)
|
|
|
|
repository.before_change_head
|
|
end
|
|
end
|
|
|
|
describe '#after_change_head' do
|
|
it 'flushes the readme cache' do
|
|
expect(repository).to receive(:expire_method_caches).with([
|
|
:readme,
|
|
:changelog,
|
|
:license,
|
|
:contributing,
|
|
:gitignore,
|
|
:koding,
|
|
:gitlab_ci,
|
|
:avatar,
|
|
:issue_template,
|
|
:merge_request_template
|
|
])
|
|
|
|
repository.after_change_head
|
|
end
|
|
end
|
|
|
|
describe '#before_push_tag' do
|
|
it 'flushes the cache' do
|
|
expect(repository).to receive(:expire_statistics_caches)
|
|
expect(repository).to receive(:expire_emptiness_caches)
|
|
expect(repository).to receive(:expire_tags_cache)
|
|
|
|
repository.before_push_tag
|
|
end
|
|
end
|
|
|
|
describe '#after_import' do
|
|
it 'flushes and builds the cache' do
|
|
expect(repository).to receive(:expire_content_cache)
|
|
|
|
repository.after_import
|
|
end
|
|
end
|
|
|
|
describe '#after_push_commit' do
|
|
it 'expires statistics caches' do
|
|
expect(repository).to receive(:expire_statistics_caches)
|
|
.and_call_original
|
|
|
|
expect(repository).to receive(:expire_branch_cache)
|
|
.with('master')
|
|
.and_call_original
|
|
|
|
repository.after_push_commit('master')
|
|
end
|
|
end
|
|
|
|
describe '#after_create_branch' do
|
|
it 'expires the branch caches' do
|
|
expect(repository).to receive(:expire_branches_cache)
|
|
|
|
repository.after_create_branch
|
|
end
|
|
end
|
|
|
|
describe '#after_remove_branch' do
|
|
it 'expires the branch caches' do
|
|
expect(repository).to receive(:expire_branches_cache)
|
|
|
|
repository.after_remove_branch
|
|
end
|
|
end
|
|
|
|
describe '#after_create' do
|
|
it 'flushes the exists cache' do
|
|
expect(repository).to receive(:expire_exists_cache)
|
|
|
|
repository.after_create
|
|
end
|
|
|
|
it 'flushes the root ref cache' do
|
|
expect(repository).to receive(:expire_root_ref_cache)
|
|
|
|
repository.after_create
|
|
end
|
|
|
|
it 'flushes the emptiness caches' do
|
|
expect(repository).to receive(:expire_emptiness_caches)
|
|
|
|
repository.after_create
|
|
end
|
|
end
|
|
|
|
describe "#copy_gitattributes" do
|
|
it 'returns true with a valid ref' do
|
|
expect(repository.copy_gitattributes('master')).to be_truthy
|
|
end
|
|
|
|
it 'returns false with an invalid ref' do
|
|
expect(repository.copy_gitattributes('invalid')).to be_falsey
|
|
end
|
|
end
|
|
|
|
describe '#before_remove_tag' do
|
|
it 'flushes the tag cache' do
|
|
expect(repository).to receive(:expire_tags_cache).and_call_original
|
|
expect(repository).to receive(:expire_statistics_caches).and_call_original
|
|
|
|
repository.before_remove_tag
|
|
end
|
|
end
|
|
|
|
describe '#branch_count' do
|
|
it 'returns the number of branches' do
|
|
expect(repository.branch_count).to be_an(Integer)
|
|
|
|
# NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
|
|
rugged_count = repository.raw_repository.rugged.branches.count
|
|
|
|
expect(repository.branch_count).to eq(rugged_count)
|
|
end
|
|
end
|
|
|
|
describe '#tag_count' do
|
|
it 'returns the number of tags' do
|
|
expect(repository.tag_count).to be_an(Integer)
|
|
|
|
# NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
|
|
rugged_count = repository.raw_repository.rugged.tags.count
|
|
|
|
expect(repository.tag_count).to eq(rugged_count)
|
|
end
|
|
end
|
|
|
|
describe '#expire_branches_cache' do
|
|
it 'expires the cache' do
|
|
expect(repository).to receive(:expire_method_caches)
|
|
.with(%i(branch_names branch_count has_visible_content?))
|
|
.and_call_original
|
|
|
|
repository.expire_branches_cache
|
|
end
|
|
end
|
|
|
|
describe '#expire_tags_cache' do
|
|
it 'expires the cache' do
|
|
expect(repository).to receive(:expire_method_caches)
|
|
.with(%i(tag_names tag_count))
|
|
.and_call_original
|
|
|
|
repository.expire_tags_cache
|
|
end
|
|
end
|
|
|
|
describe '#add_tag' do
|
|
let(:user) { build_stubbed(:user) }
|
|
|
|
shared_examples 'adding tag' do
|
|
context 'with a valid target' do
|
|
it 'creates the tag' do
|
|
repository.add_tag(user, '8.5', 'master', 'foo')
|
|
|
|
tag = repository.find_tag('8.5')
|
|
expect(tag).to be_present
|
|
expect(tag.message).to eq('foo')
|
|
expect(tag.dereferenced_target.id).to eq(repository.commit('master').id)
|
|
end
|
|
|
|
it 'returns a Gitlab::Git::Tag object' do
|
|
tag = repository.add_tag(user, '8.5', 'master', 'foo')
|
|
|
|
expect(tag).to be_a(Gitlab::Git::Tag)
|
|
end
|
|
end
|
|
|
|
context 'with an invalid target' do
|
|
it 'returns false' do
|
|
expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly operation_user_add_tag feature is enabled' do
|
|
it_behaves_like 'adding tag'
|
|
end
|
|
|
|
context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do
|
|
it_behaves_like 'adding tag'
|
|
|
|
it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
|
|
pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
|
|
update_hook = Gitlab::Git::Hook.new('update', project)
|
|
post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)
|
|
|
|
allow(Gitlab::Git::Hook).to receive(:new)
|
|
.and_return(pre_receive_hook, update_hook, post_receive_hook)
|
|
|
|
allow(pre_receive_hook).to receive(:trigger).and_call_original
|
|
allow(update_hook).to receive(:trigger).and_call_original
|
|
allow(post_receive_hook).to receive(:trigger).and_call_original
|
|
|
|
tag = repository.add_tag(user, '8.5', 'master', 'foo')
|
|
|
|
commit_sha = repository.commit('master').id
|
|
tag_sha = tag.target
|
|
|
|
expect(pre_receive_hook).to have_received(:trigger)
|
|
.with(anything, anything, anything, commit_sha, anything)
|
|
expect(update_hook).to have_received(:trigger)
|
|
.with(anything, anything, anything, commit_sha, anything)
|
|
expect(post_receive_hook).to have_received(:trigger)
|
|
.with(anything, anything, anything, tag_sha, anything)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#rm_branch' do
|
|
shared_examples "user deleting a branch" do
|
|
it 'removes a branch' do
|
|
expect(repository).to receive(:before_remove_branch)
|
|
expect(repository).to receive(:after_remove_branch)
|
|
|
|
repository.rm_branch(user, 'feature')
|
|
end
|
|
end
|
|
|
|
context 'with gitaly enabled' do
|
|
it_behaves_like "user deleting a branch"
|
|
|
|
context 'when pre hooks failed' do
|
|
before do
|
|
allow_any_instance_of(Gitlab::GitalyClient::OperationService)
|
|
.to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError)
|
|
end
|
|
|
|
it 'gets an error and does not delete the branch' do
|
|
expect do
|
|
repository.rm_branch(user, 'feature')
|
|
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
|
|
|
|
expect(repository.find_branch('feature')).not_to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'with gitaly disabled', :skip_gitaly_mock do
|
|
it_behaves_like "user deleting a branch"
|
|
|
|
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
|
|
let(:blank_sha) { '0000000000000000000000000000000000000000' }
|
|
|
|
context 'when pre hooks were successful' do
|
|
it 'runs without errors' do
|
|
expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
|
|
.with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')
|
|
|
|
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
|
|
end
|
|
|
|
it 'deletes the branch' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
|
|
|
|
expect { repository.rm_branch(user, 'feature') }.not_to raise_error
|
|
|
|
expect(repository.find_branch('feature')).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when pre hooks failed' do
|
|
it 'gets an error' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
|
|
|
|
expect do
|
|
repository.rm_branch(user, 'feature')
|
|
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
|
|
end
|
|
|
|
it 'does not delete the branch' do
|
|
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
|
|
|
|
expect do
|
|
repository.rm_branch(user, 'feature')
|
|
end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
|
|
expect(repository.find_branch('feature')).not_to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#rm_tag' do
|
|
shared_examples 'removing tag' do
|
|
it 'removes a tag' do
|
|
expect(repository).to receive(:before_remove_tag)
|
|
|
|
repository.rm_tag(build_stubbed(:user), 'v1.1.0')
|
|
|
|
expect(repository.find_tag('v1.1.0')).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when Gitaly operation_user_delete_tag feature is enabled' do
|
|
it_behaves_like 'removing tag'
|
|
end
|
|
|
|
context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
|
|
it_behaves_like 'removing tag'
|
|
end
|
|
end
|
|
|
|
describe '#avatar' do
|
|
it 'returns nil if repo does not exist' do
|
|
expect(repository).to receive(:file_on_head)
|
|
.and_raise(Rugged::ReferenceError)
|
|
|
|
expect(repository.avatar).to eq(nil)
|
|
end
|
|
|
|
it 'returns the first avatar file found in the repository' do
|
|
expect(repository).to receive(:file_on_head)
|
|
.with(:avatar)
|
|
.and_return(double(:tree, path: 'logo.png'))
|
|
|
|
expect(repository.avatar).to eq('logo.png')
|
|
end
|
|
|
|
it 'caches the output' do
|
|
expect(repository).to receive(:file_on_head)
|
|
.with(:avatar)
|
|
.once
|
|
.and_return(double(:tree, path: 'logo.png'))
|
|
|
|
2.times { expect(repository.avatar).to eq('logo.png') }
|
|
end
|
|
end
|
|
|
|
describe '#expire_exists_cache' do
|
|
let(:cache) { repository.send(:cache) }
|
|
|
|
it 'expires the cache' do
|
|
expect(cache).to receive(:expire).with(:exists?)
|
|
|
|
repository.expire_exists_cache
|
|
end
|
|
end
|
|
|
|
describe "#keep_around" do
|
|
it "does not fail if we attempt to reference bad commit" do
|
|
expect(repository.kept_around?('abc1234')).to be_falsey
|
|
end
|
|
|
|
it "stores a reference to the specified commit sha so it isn't garbage collected" do
|
|
repository.keep_around(sample_commit.id)
|
|
|
|
expect(repository.kept_around?(sample_commit.id)).to be_truthy
|
|
end
|
|
|
|
it "attempting to call keep_around on truncated ref does not fail" do
|
|
repository.keep_around(sample_commit.id)
|
|
ref = repository.send(:keep_around_ref_name, sample_commit.id)
|
|
path = File.join(repository.path, ref)
|
|
# Corrupt the reference
|
|
File.truncate(path, 0)
|
|
|
|
expect(repository.kept_around?(sample_commit.id)).to be_falsey
|
|
|
|
repository.keep_around(sample_commit.id)
|
|
|
|
expect(repository.kept_around?(sample_commit.id)).to be_falsey
|
|
|
|
File.delete(path)
|
|
end
|
|
end
|
|
|
|
describe '#update_ref' do
|
|
it 'can create a ref' do
|
|
Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
|
|
|
|
expect(repository.find_branch('foobar')).not_to be_nil
|
|
end
|
|
|
|
it 'raises CommitError when the ref update fails' do
|
|
expect do
|
|
Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
|
|
end.to raise_error(Gitlab::Git::CommitError)
|
|
end
|
|
end
|
|
|
|
describe '#contribution_guide', :use_clean_rails_memory_store_caching do
|
|
it 'returns and caches the output' do
|
|
expect(repository).to receive(:file_on_head)
|
|
.with(:contributing)
|
|
.and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md'))
|
|
.once
|
|
|
|
2.times do
|
|
expect(repository.contribution_guide)
|
|
.to be_an_instance_of(Gitlab::Git::Tree)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#gitignore', :use_clean_rails_memory_store_caching do
|
|
it 'returns and caches the output' do
|
|
expect(repository).to receive(:file_on_head)
|
|
.with(:gitignore)
|
|
.and_return(Gitlab::Git::Tree.new(path: '.gitignore'))
|
|
.once
|
|
|
|
2.times do
|
|
expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#koding_yml', :use_clean_rails_memory_store_caching do
|
|
it 'returns and caches the output' do
|
|
expect(repository).to receive(:file_on_head)
|
|
.with(:koding)
|
|
.and_return(Gitlab::Git::Tree.new(path: '.koding.yml'))
|
|
.once
|
|
|
|
2.times do
|
|
expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#readme', :use_clean_rails_memory_store_caching do
|
|
context 'with a non-existing repository' do
|
|
it 'returns nil' do
|
|
allow(repository).to receive(:tree).with(:head).and_return(nil)
|
|
|
|
expect(repository.readme).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
context 'when no README exists' do
|
|
it 'returns nil' do
|
|
allow_any_instance_of(Tree).to receive(:readme).and_return(nil)
|
|
|
|
expect(repository.readme).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when a README exists' do
|
|
it 'returns the README' do
|
|
expect(repository.readme).to be_an_instance_of(ReadmeBlob)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#expire_statistics_caches' do
|
|
it 'expires the caches' do
|
|
expect(repository).to receive(:expire_method_caches)
|
|
.with(%i(size commit_count))
|
|
|
|
repository.expire_statistics_caches
|
|
end
|
|
end
|
|
|
|
describe '#expire_method_caches' do
|
|
it 'expires the caches of the given methods' do
|
|
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme)
|
|
expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore)
|
|
|
|
repository.expire_method_caches(%i(readme gitignore))
|
|
end
|
|
end
|
|
|
|
describe '#expire_all_method_caches' do
|
|
it 'expires the caches of all methods' do
|
|
expect(repository).to receive(:expire_method_caches)
|
|
.with(Repository::CACHED_METHODS)
|
|
|
|
repository.expire_all_method_caches
|
|
end
|
|
|
|
it 'all cache_method definitions are in the lists of method caches' do
|
|
methods = repository.methods.map do |method|
|
|
match = /^_uncached_(.*)/.match(method)
|
|
match[1].to_sym if match
|
|
end.compact
|
|
|
|
expect(methods).to match_array(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS)
|
|
end
|
|
end
|
|
|
|
describe '#file_on_head' do
|
|
context 'with a non-existing repository' do
|
|
it 'returns nil' do
|
|
expect(repository).to receive(:tree).with(:head).and_return(nil)
|
|
|
|
expect(repository.file_on_head(:readme)).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'with a repository that has no blobs' do
|
|
it 'returns nil' do
|
|
expect_any_instance_of(Tree).to receive(:blobs).and_return([])
|
|
|
|
expect(repository.file_on_head(:readme)).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
it 'returns a Gitlab::Git::Tree' do
|
|
expect(repository.file_on_head(:readme))
|
|
.to be_an_instance_of(Gitlab::Git::Tree)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#head_tree' do
|
|
context 'with an existing repository' do
|
|
it 'returns a Tree' do
|
|
expect(repository.head_tree).to be_an_instance_of(Tree)
|
|
end
|
|
end
|
|
|
|
context 'with a non-existing repository' do
|
|
it 'returns nil' do
|
|
expect(repository).to receive(:head_commit).and_return(nil)
|
|
|
|
expect(repository.head_tree).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#tree' do
|
|
context 'using a non-existing repository' do
|
|
before do
|
|
allow(repository).to receive(:head_commit).and_return(nil)
|
|
end
|
|
|
|
it 'returns nil' do
|
|
expect(repository.tree(:head)).to be_nil
|
|
end
|
|
|
|
it 'returns nil when using a path' do
|
|
expect(repository.tree(:head, 'README.md')).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'using an existing repository' do
|
|
it 'returns a Tree' do
|
|
expect(repository.tree(:head)).to be_an_instance_of(Tree)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#size' do
|
|
context 'with a non-existing repository' do
|
|
it 'returns 0' do
|
|
expect(repository).to receive(:exists?).and_return(false)
|
|
|
|
expect(repository.size).to eq(0.0)
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
it 'returns the repository size as a Float' do
|
|
expect(repository.size).to be_an_instance_of(Float)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#commit_count' do
|
|
context 'with a non-existing repository' do
|
|
it 'returns 0' do
|
|
expect(repository).to receive(:root_ref).and_return(nil)
|
|
|
|
expect(repository.commit_count).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
it 'returns the commit count' do
|
|
expect(repository.commit_count).to be_an(Integer)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#commit_count_for_ref' do
|
|
let(:project) { create :project }
|
|
|
|
context 'with a non-existing repository' do
|
|
it 'returns 0' do
|
|
expect(project.repository.commit_count_for_ref('master')).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'with empty repository' do
|
|
it 'returns 0' do
|
|
project.create_repository
|
|
expect(project.repository.commit_count_for_ref('master')).to eq(0)
|
|
end
|
|
end
|
|
|
|
context 'when searching for the root ref' do
|
|
it 'returns the same count as #commit_count' do
|
|
expect(repository.commit_count_for_ref(repository.root_ref)).to eq(repository.commit_count)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#cache_method_output', :use_clean_rails_memory_store_caching do
|
|
let(:fallback) { 10 }
|
|
|
|
context 'with a non-existing repository' do
|
|
let(:project) { create(:project) } # No repository
|
|
|
|
subject do
|
|
repository.cache_method_output(:cats, fallback: fallback) do
|
|
repository.cats_call_stub
|
|
end
|
|
end
|
|
|
|
it 'returns the fallback value' do
|
|
expect(subject).to eq(fallback)
|
|
end
|
|
|
|
it 'avoids calling the original method' do
|
|
expect(repository).not_to receive(:cats_call_stub)
|
|
|
|
subject
|
|
end
|
|
end
|
|
|
|
context 'with a method throwing a non-existing-repository error' do
|
|
subject do
|
|
repository.cache_method_output(:cats, fallback: fallback) do
|
|
raise Gitlab::Git::Repository::NoRepository
|
|
end
|
|
end
|
|
|
|
it 'returns the fallback value' do
|
|
expect(subject).to eq(fallback)
|
|
end
|
|
|
|
it 'does not cache the data' do
|
|
subject
|
|
|
|
expect(repository.instance_variable_defined?(:@cats)).to eq(false)
|
|
expect(repository.send(:cache).exist?(:cats)).to eq(false)
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
it 'caches the output' do
|
|
object = double
|
|
|
|
expect(object).to receive(:number).once.and_return(10)
|
|
|
|
2.times do
|
|
val = repository.cache_method_output(:cats) { object.number }
|
|
|
|
expect(val).to eq(10)
|
|
end
|
|
|
|
expect(repository.send(:cache).exist?(:cats)).to eq(true)
|
|
expect(repository.instance_variable_get(:@cats)).to eq(10)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#refresh_method_caches' do
|
|
it 'refreshes the caches of the given types' do
|
|
expect(repository).to receive(:expire_method_caches)
|
|
.with(%i(rendered_readme license_blob license_key license))
|
|
|
|
expect(repository).to receive(:rendered_readme)
|
|
expect(repository).to receive(:license_blob)
|
|
expect(repository).to receive(:license_key)
|
|
expect(repository).to receive(:license)
|
|
|
|
repository.refresh_method_caches(%i(readme license))
|
|
end
|
|
end
|
|
|
|
describe '#gitlab_ci_yml_for' do
|
|
before do
|
|
repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
|
|
end
|
|
|
|
context 'when there is a .gitlab-ci.yml at the commit' do
|
|
it 'returns the content' do
|
|
expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
|
|
end
|
|
end
|
|
|
|
context 'when there is no .gitlab-ci.yml at the commit' do
|
|
it 'returns nil' do
|
|
expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#route_map_for' do
|
|
before do
|
|
repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master')
|
|
end
|
|
|
|
context 'when there is a .gitlab/route-map.yml at the commit' do
|
|
it 'returns the content' do
|
|
expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT')
|
|
end
|
|
end
|
|
|
|
context 'when there is no .gitlab/route-map.yml at the commit' do
|
|
it 'returns nil' do
|
|
expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#ancestor?' do
|
|
let(:commit) { repository.commit }
|
|
let(:ancestor) { commit.parents.first }
|
|
|
|
context 'with Gitaly enabled' do
|
|
it 'it is an ancestor' do
|
|
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
|
|
end
|
|
|
|
it 'it is not an ancestor' do
|
|
expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
|
|
end
|
|
|
|
it 'returns false on nil-values' do
|
|
expect(repository.ancestor?(nil, commit.id)).to eq(false)
|
|
expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
|
|
expect(repository.ancestor?(nil, nil)).to eq(false)
|
|
end
|
|
end
|
|
|
|
context 'with Gitaly disabled' do
|
|
before do
|
|
allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false)
|
|
allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false)
|
|
end
|
|
|
|
it 'it is an ancestor' do
|
|
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
|
|
end
|
|
|
|
it 'it is not an ancestor' do
|
|
expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
|
|
end
|
|
|
|
it 'returns false on nil-values' do
|
|
expect(repository.ancestor?(nil, commit.id)).to eq(false)
|
|
expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
|
|
expect(repository.ancestor?(nil, nil)).to eq(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'commit cache' do
|
|
set(:project) { create(:project, :repository) }
|
|
|
|
it 'caches based on SHA' do
|
|
# Gets the commit oid, and warms the cache
|
|
oid = project.commit.id
|
|
|
|
expect(Gitlab::Git::Commit).not_to receive(:find).once
|
|
|
|
project.commit_by(oid: oid)
|
|
end
|
|
|
|
it 'caches nil values' do
|
|
expect(Gitlab::Git::Commit).to receive(:find).once
|
|
|
|
project.commit_by(oid: '1' * 40)
|
|
project.commit_by(oid: '1' * 40)
|
|
end
|
|
end
|
|
|
|
describe '#raw_repository' do
|
|
subject { repository.raw_repository }
|
|
|
|
it 'returns a Gitlab::Git::Repository representation of the repository' do
|
|
expect(subject).to be_a(Gitlab::Git::Repository)
|
|
expect(subject.relative_path).to eq(project.disk_path + '.git')
|
|
expect(subject.gl_repository).to eq("project-#{project.id}")
|
|
end
|
|
|
|
context 'with a wiki repository' do
|
|
let(:repository) { project.wiki.repository }
|
|
|
|
it 'creates a Gitlab::Git::Repository with the proper attributes' do
|
|
expect(subject).to be_a(Gitlab::Git::Repository)
|
|
expect(subject.relative_path).to eq(project.disk_path + '.wiki.git')
|
|
expect(subject.gl_repository).to eq("wiki-#{project.id}")
|
|
end
|
|
end
|
|
end
|
|
end
|