gitlab-org--gitlab-foss/spec/models/repository_spec.rb

2543 lines
78 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_desc' do
subject { repository.tags_sorted_by('name_desc').map(&:name) }
it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
end
context 'name_asc' do
subject { repository.tags_sorted_by('name_asc').map(&:name) }
it { is_expected.to eq(['v1.0.0', 'v1.1.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 '#commits_by' do
set(:project) { create(:project, :repository) }
shared_examples 'batch commits fetching' do
let(:oids) { TestEnv::BRANCH_SHA.values }
subject { project.repository.commits_by(oids: oids) }
it 'finds each commit' do
expect(subject).not_to include(nil)
expect(subject.size).to eq(oids.size)
end
it 'returns only Commit instances' do
expect(subject).to all( be_a(Commit) )
end
context 'when some commits are not found ' do
let(:oids) do
['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10)
end
it 'returns only found commits' do
expect(subject).not_to include(nil)
expect(subject.size).to eq(10)
end
end
context 'when no oids are passed' do
let(:oids) { [] }
it 'does not call #batch_by_oid' do
expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid)
subject
end
end
end
context 'when Gitaly list_commits_by_oid is enabled' do
it_behaves_like 'batch commits fetching'
end
context 'when Gitaly list_commits_by_oid is enabled', :disable_gitaly do
it_behaves_like 'batch commits fetching'
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
shared_examples '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 without conflict sides missing' do
subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') }
it { is_expected.to be_falsey }
end
context 'non-mergeable branches with conflict sides missing' do
subject { repository.can_be_merged?('conflict-missing-side', 'conflict-start') }
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
context 'when Gitaly can_be_merged feature is enabled' do
it_behaves_like 'can be merged'
end
context 'when Gitaly can_be_merged feature is disabled', :disable_gitaly do
it_behaves_like 'can be merged'
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_hooks' do
let(:hook_path) { File.join(repository.path_to_repo, 'hooks') }
it 'symlinks the global hooks directory' do
repository.create_hooks
expect(File.symlink?(hook_path)).to be true
expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
end
it 'replaces existing symlink with the right directory' do
FileUtils.mkdir_p(hook_path)
expect(File.symlink?(hook_path)).to be false
repository.create_hooks
expect(File.symlink?(hook_path)).to be true
expect(File.readlink(hook_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
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 "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\x00190\x00 - 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 'ignores leading slashes' do
results = repository.search_files_by_name('/files', 'master')
expect(results.first).to eq('files/html/500.html')
end
it 'properly handles when query is only slashes' do
results = repository.search_files_by_name('//', 'master')
expect(results).to match_array([])
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.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.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
subject do
Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('new-feature') do
new_rev
end
end
it 'returns branch_created as true' do
expect(subject).not_to be_repo_created
expect(subject).to be_branch_created
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 '#tag_exists?' do
it 'uses tag_names' do
allow(repository).to receive(:tag_names).and_return(['foobar'])
expect(repository.tag_exists?('foobar')).to eq(true)
expect(repository.tag_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 '#diverging_commit_counts' do
it 'returns the commit counts behind and ahead of default branch' do
result = repository.diverging_commit_counts(
repository.find_branch('fix'))
expect(result).to eq(behind: 29, ahead: 2)
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
describe '#contributors' do
let(:author_a) { build(:author, email: 'tiagonbotelho@hotmail.com', name: 'tiagonbotelho') }
let(:author_b) { build(:author, email: 'gitlab@winniehell.de', name: 'Winnie') }
let(:author_c) { build(:author, email: 'douwe@gitlab.com', name: 'Douwe Maan') }
let(:stubbed_commits) do
[build(:commit, author: author_a),
build(:commit, author: author_a),
build(:commit, author: author_b),
build(:commit, author: author_c),
build(:commit, author: author_c),
build(:commit, author: author_c)]
end
let(:order_by) { nil }
let(:sort) { nil }
before do
allow(repository).to receive(:commits).with(nil, limit: 2000, offset: 0, skip_merges: true).and_return(stubbed_commits)
end
subject { repository.contributors(order_by: order_by, sort: sort) }
def expect_contributors(*contributors)
expect(subject.map(&:email)).to eq(contributors.map(&:email))
end
it 'returns the array of Gitlab::Contributor for the repository' do
expect_contributors(author_a, author_b, author_c)
end
context 'order_by email' do
let(:order_by) { 'email' }
context 'asc' do
let(:sort) { 'asc' }
it 'returns all the contributors ordered by email asc case insensitive' do
expect_contributors(author_c, author_b, author_a)
end
end
context 'desc' do
let(:sort) { 'desc' }
it 'returns all the contributors ordered by email desc case insensitive' do
expect_contributors(author_a, author_b, author_c)
end
end
end
context 'order_by name' do
let(:order_by) { 'name' }
context 'asc' do
let(:sort) { 'asc' }
it 'returns all the contributors ordered by name asc case insensitive' do
expect_contributors(author_c, author_a, author_b)
end
end
context 'desc' do
let(:sort) { 'desc' }
it 'returns all the contributors ordered by name desc case insensitive' do
expect_contributors(author_b, author_a, author_c)
end
end
end
context 'order_by commits' do
let(:order_by) { 'commits' }
context 'asc' do
let(:sort) { 'asc' }
it 'returns all the contributors ordered by commits asc' do
expect_contributors(author_b, author_a, author_c)
end
end
context 'desc' do
let(:sort) { 'desc' }
it 'returns all the contributors ordered by commits desc' do
expect_contributors(author_c, author_a, author_b)
end
end
end
context 'invalid ordering' do
let(:order_by) { 'unknown' }
it 'returns the contributors unsorted' do
expect_contributors(author_a, author_b, author_c)
end
end
context 'invalid sorting' do
let(:order_by) { 'name' }
let(:sort) { 'unknown' }
it 'returns the contributors unsorted' do
expect_contributors(author_a, author_b, author_c)
end
end
end
end