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