f14647fdae
Previously `ProjectCacheWorker` would be scheduled once per ref, which would generate unnecessary I/O and load on Sidekiq, especially if many tags or branches were pushed at once. `ProjectCacheWorker` would expire three items: 1. Repository size: This only needs to be updated once per push. 2. Commit count: This only needs to be updated if the default branch is updated. 3. Project method caches: This only needs to be updated if the default branch changes, but only if certain files change (e.g. README, CHANGELOG, etc.). Because the third item requires looking at the actual changes in the commit deltas, we schedule one `ProjectCacheWorker` to handle the first two cases, and schedule a separate `ProjectCacheWorker` for the third case if it is needed. As a result, this brings down the number of `ProjectCacheWorker` jobs from N to 2. Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/52046
2682 lines
81 KiB
Ruby
2682 lines
81 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'spec_helper'
|
|
|
|
describe Repository do
|
|
include RepoHelpers
|
|
include GitHelpers
|
|
|
|
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::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
|
|
set(:project) { create(:project, :repository) }
|
|
let(:repository) { project.repository }
|
|
|
|
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 'raises 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' } }
|
|
|
|
rugged_repo(repository).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
|
|
rugged_repo(repository).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 '#list_last_commits_for_tree' do
|
|
let(:path_to_commit) do
|
|
{
|
|
"encoding" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
|
|
"files" => "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
|
|
".gitignore" => "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
|
|
".gitmodules" => "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
|
|
"CHANGELOG" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
|
|
"CONTRIBUTING.md" => "6d394385cf567f80a8fd85055db1ab4c5295806f",
|
|
"Gemfile.zip" => "ae73cb07c9eeaf35924a10f713b364d32b2dd34f",
|
|
"LICENSE" => "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863",
|
|
"MAINTENANCE.md" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
|
|
"PROCESS.md" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
|
|
"README.md" => "1a0b36b3cdad1d2ee32457c102a8c0b7056fa863",
|
|
"VERSION" => "913c66a37b4a45b9769037c55c2d238bd0942d2e",
|
|
"gitlab-shell" => "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
|
|
"six" => "cfe32cf61b73a0d5e9f13e774abde7ff789b1660"
|
|
}
|
|
end
|
|
|
|
subject { repository.list_last_commits_for_tree(sample_commit.id, '.').id }
|
|
|
|
it 'returns the last commits for every entry in the current path' do
|
|
result = repository.list_last_commits_for_tree(sample_commit.id, '.')
|
|
|
|
result.each do |key, value|
|
|
result[key] = value.id
|
|
end
|
|
|
|
expect(result).to include(path_to_commit)
|
|
end
|
|
|
|
it 'returns the last commits for every entry in the current path starting from the offset' do
|
|
result = repository.list_last_commits_for_tree(sample_commit.id, '.', offset: path_to_commit.size - 1)
|
|
|
|
expect(result.size).to eq(1)
|
|
end
|
|
|
|
it 'returns a limited number of last commits for every entry in the current path starting from the offset' do
|
|
result = repository.list_last_commits_for_tree(sample_commit.id, '.', limit: 1)
|
|
|
|
expect(result.size).to eq(1)
|
|
end
|
|
|
|
it 'returns an empty hash when offset is out of bounds' do
|
|
result = repository.list_last_commits_for_tree(sample_commit.id, '.', offset: path_to_commit.size)
|
|
|
|
expect(result.size).to eq(0)
|
|
end
|
|
|
|
context 'with a commit with invalid UTF-8 path' do
|
|
def create_commit_with_invalid_utf8_path
|
|
rugged = rugged_repo(repository)
|
|
blob_id = Rugged::Blob.from_buffer(rugged, "some contents")
|
|
tree_builder = Rugged::Tree::Builder.new(rugged)
|
|
tree_builder.insert({ oid: blob_id, name: "hello\x80world", filemode: 0100644 })
|
|
tree_id = tree_builder.write
|
|
user = { email: "jcai@gitlab.com", time: Time.now, name: "John Cai" }
|
|
|
|
Rugged::Commit.create(rugged, message: 'some commit message', parents: [rugged.head.target.oid], tree: tree_id, committer: user, author: user)
|
|
end
|
|
|
|
it 'does not raise an error' do
|
|
commit = create_commit_with_invalid_utf8_path
|
|
|
|
expect { repository.list_last_commits_for_tree(commit, '.', offset: 0) }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#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 'raises 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
|
|
|
|
describe '#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 'raises 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
|
|
|
|
describe '#commits' do
|
|
context 'when neither the all flag nor a ref are specified' do
|
|
it 'returns every commit from default branch' do
|
|
expect(repository.commits(limit: 60).size).to eq(37)
|
|
end
|
|
end
|
|
|
|
context 'when ref is passed' do
|
|
it 'returns every commit from the specified ref' do
|
|
expect(repository.commits('master', limit: 60).size).to eq(37)
|
|
end
|
|
|
|
context 'when all' do
|
|
it 'returns every commit from the repository' do
|
|
expect(repository.commits('master', limit: 60, all: true).size).to eq(60)
|
|
end
|
|
end
|
|
|
|
context 'with path' do
|
|
it 'sets follow when it 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', limit: 1, path: 'README.md')
|
|
repository.commits('master', limit: 1, path: ['README.md'])
|
|
end
|
|
|
|
it 'does not set follow when it is multiple paths' do
|
|
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
|
|
|
|
repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG'])
|
|
end
|
|
end
|
|
|
|
context 'without path' do
|
|
it 'does not set follow' do
|
|
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
|
|
|
|
repository.commits('master', limit: 1)
|
|
end
|
|
end
|
|
end
|
|
|
|
context "when 'all' flag is set" do
|
|
it 'returns every commit from the repository' do
|
|
expect(repository.commits(all: true, limit: 60).size).to eq(60)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#new_commits' do
|
|
set(:project) { create(:project, :repository) }
|
|
let(:repository) { project.repository }
|
|
|
|
subject { repository.new_commits(rev) }
|
|
|
|
context 'when there are no new commits' do
|
|
let(:rev) { repository.commit.id }
|
|
|
|
it 'returns an empty array' do
|
|
expect(subject).to eq([])
|
|
end
|
|
end
|
|
|
|
context 'when new commits are found' do
|
|
let(:branch) { 'orphaned-branch' }
|
|
let!(:rev) { repository.commit(branch).id }
|
|
|
|
it 'returns the commits' do
|
|
repository.delete_branch(branch)
|
|
|
|
expect(subject).not_to be_empty
|
|
expect(subject).to all( be_a(::Commit) )
|
|
expect(subject.size).to eq(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#commits_by' do
|
|
set(:project) { create(:project, :repository) }
|
|
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
|
|
|
|
describe '#find_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
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'raises 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
|
|
|
|
context 'regular blob' do
|
|
subject { repository.blob_at(repository.head_commit.sha, '.gitignore') }
|
|
|
|
it { is_expected.to be_an_instance_of(::Blob) }
|
|
end
|
|
|
|
context 'readme blob on HEAD' do
|
|
subject { repository.blob_at(repository.head_commit.sha, 'README.md') }
|
|
|
|
it { is_expected.to be_an_instance_of(::ReadmeBlob) }
|
|
end
|
|
|
|
context 'readme blob not on HEAD' do
|
|
subject { repository.blob_at(repository.find_branch('feature').target, 'README.md') }
|
|
|
|
it { is_expected.to be_an_instance_of(::Blob) }
|
|
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
|
|
|
|
context 'non merged branch' do
|
|
subject { repository.merged_to_root_ref?('fix') }
|
|
|
|
it { is_expected.to be_falsey }
|
|
end
|
|
|
|
context 'non existent branch' do
|
|
subject { repository.merged_to_root_ref?('non_existent_branch') }
|
|
|
|
it { is_expected.to be_nil }
|
|
end
|
|
end
|
|
|
|
describe '#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 'submodule changes that confuse rugged' do
|
|
subject { repository.can_be_merged?('update-gitlab-shell-v-6-0-1', 'update-gitlab-shell-v-6-0-3') }
|
|
|
|
it { is_expected.to be_falsey }
|
|
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 specified' do
|
|
it 'is using a root ref' do
|
|
expect(repository).to receive(:find_commit).with('master')
|
|
|
|
repository.commit
|
|
end
|
|
end
|
|
|
|
context 'when ref is not valid' do
|
|
context 'when preceding tree element exists' do
|
|
it 'returns nil' do
|
|
expect(repository.commit('master:ref')).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when preceding tree element does not exist' do
|
|
it 'returns nil' do
|
|
expect(repository.commit('non-existent:ref')).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#create_dir" do
|
|
it "commits a change that creates a new directory" do
|
|
expect do
|
|
repository.create_dir(user, 'newdir',
|
|
message: 'Create newdir', branch_name: 'master')
|
|
end.to change { repository.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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.count_commits(ref: 'master') }.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 'raises 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 'raises a storage error' do
|
|
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#async_remove_remote' do
|
|
before do
|
|
masterrev = repository.find_branch('master').dereferenced_target
|
|
create_remote_branch('joe', 'remote_branch', masterrev)
|
|
end
|
|
|
|
context 'when worker is scheduled successfully' do
|
|
before do
|
|
masterrev = repository.find_branch('master').dereferenced_target
|
|
create_remote_branch('remote_name', 'remote_branch', masterrev)
|
|
|
|
allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return('1234')
|
|
end
|
|
|
|
it 'returns job_id' do
|
|
expect(repository.async_remove_remote('joe')).to eq('1234')
|
|
end
|
|
end
|
|
|
|
context 'when worker does not schedule successfully' do
|
|
before do
|
|
allow(RepositoryRemoveRemoteWorker).to receive(:perform_async).and_return(nil)
|
|
end
|
|
|
|
it 'returns nil' do
|
|
expect(Rails.logger).to receive(:info).with("Remove remote job failed to create for #{project.id} with remote name joe.")
|
|
|
|
expect(repository.async_remove_remote('joe')).to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#fetch_ref' do
|
|
let(:broken_repository) { create(:project, :broken_storage).repository }
|
|
|
|
describe 'when storage is broken', :broken_storage do
|
|
it 'raises 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 '#get_raw_changes' do
|
|
context `with non-UTF8 bytes in paths` do
|
|
let(:old_rev) { 'd0888d297eadcd7a345427915c309413b1231e65' }
|
|
let(:new_rev) { '19950f03c765f7ac8723a73a0599764095f52fc0' }
|
|
let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
|
|
|
|
it 'returns the changes' do
|
|
expect { changes }.not_to raise_error
|
|
expect(changes.first.new_path.bytes).to eq("hello\x80world".bytes)
|
|
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(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
|
|
|
|
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', 'Gitlab B.V.',
|
|
message: 'Add LICENSE', branch_name: 'master')
|
|
|
|
expect(repository.license_key).to be_nil
|
|
end
|
|
|
|
it 'returns nil when the commit SHA does not exist' do
|
|
allow(repository.head_commit).to receive(:sha).and_return('1' * 40)
|
|
|
|
expect(repository.license_key).to be_nil
|
|
end
|
|
|
|
it 'returns nil when master does not exist' do
|
|
repository.rm_branch(user, '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', 'Gitlab B.V.',
|
|
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(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
|
|
expect(repository.gitlab_ci_yml).to be_nil
|
|
end
|
|
end
|
|
|
|
describe '#ambiguous_ref?' do
|
|
let(:ref) { 'ref' }
|
|
|
|
subject { repository.ambiguous_ref?(ref) }
|
|
|
|
context 'when ref is ambiguous' do
|
|
before do
|
|
repository.add_tag(project.creator, ref, 'master')
|
|
repository.add_branch(project.creator, ref, 'master')
|
|
end
|
|
|
|
it 'is true' do
|
|
is_expected.to eq(true)
|
|
end
|
|
end
|
|
|
|
context 'when ref is not ambiguous' do
|
|
before do
|
|
repository.add_tag(project.creator, ref, 'master')
|
|
end
|
|
|
|
it 'is false' do
|
|
is_expected.to eq(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#expand_ref' do
|
|
let(:ref) { 'ref' }
|
|
|
|
subject { repository.expand_ref(ref) }
|
|
|
|
context 'when ref is not tag or branch name' do
|
|
let(:ref) { 'refs/heads/master' }
|
|
|
|
it 'returns nil' do
|
|
is_expected.to eq(nil)
|
|
end
|
|
end
|
|
|
|
context 'when ref is tag name' do
|
|
before do
|
|
repository.add_tag(project.creator, ref, 'master')
|
|
end
|
|
|
|
it 'returns the tag ref' do
|
|
is_expected.to eq("refs/tags/#{ref}")
|
|
end
|
|
end
|
|
|
|
context 'when ref is branch name' do
|
|
before do
|
|
repository.add_branch(project.creator, ref, 'master')
|
|
end
|
|
|
|
it 'returns the branch ref' do
|
|
is_expected.to eq("refs/heads/#{ref}")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#add_branch' do
|
|
let(:branch_name) { 'new_feature' }
|
|
let(:target) { 'master' }
|
|
|
|
subject { repository.add_branch(user, branch_name, target) }
|
|
|
|
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
|
|
|
|
shared_examples 'asymmetric cached method' do |method|
|
|
context 'asymmetric caching', :use_clean_rails_memory_store_caching, :request_store do
|
|
let(:cache) { repository.send(:cache) }
|
|
let(:request_store_cache) { repository.send(:request_store_cache) }
|
|
|
|
context 'when it returns true' do
|
|
before do
|
|
expect(repository.raw_repository).to receive(method).once.and_return(true)
|
|
end
|
|
|
|
it 'caches the output in RequestStore' do
|
|
expect do
|
|
repository.send(method)
|
|
end.to change { request_store_cache.read(method) }.from(nil).to(true)
|
|
end
|
|
|
|
it 'caches the output in RepositoryCache' do
|
|
expect do
|
|
repository.send(method)
|
|
end.to change { cache.read(method) }.from(nil).to(true)
|
|
end
|
|
end
|
|
|
|
context 'when it returns false' do
|
|
before do
|
|
expect(repository.raw_repository).to receive(method).once.and_return(false)
|
|
end
|
|
|
|
it 'caches the output in RequestStore' do
|
|
expect do
|
|
repository.send(method)
|
|
end.to change { request_store_cache.read(method) }.from(nil).to(false)
|
|
end
|
|
|
|
it 'does NOT cache the output in RepositoryCache' do
|
|
expect do
|
|
repository.send(method)
|
|
end.not_to change { cache.read(method) }.from(nil)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#exists?' do
|
|
it 'returns true when a repository exists' do
|
|
expect(repository.exists?).to be(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 be(false)
|
|
end
|
|
|
|
context 'with broken storage', :broken_storage do
|
|
it 'raises a storage error' do
|
|
expect_to_raise_storage_error { broken_repository.exists? }
|
|
end
|
|
end
|
|
|
|
it_behaves_like 'asymmetric cached method', :exists?
|
|
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 '#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 '#blobs_at' do
|
|
let(:empty_repository) { create(:project_empty_repo).repository }
|
|
|
|
it 'returns empty array for an empty repository' do
|
|
# rubocop:disable Style/WordArray
|
|
expect(empty_repository.blobs_at(['master', 'foobar'])).to eq([])
|
|
# rubocop:enable Style/WordArray
|
|
end
|
|
|
|
it 'returns blob array for a non-empty repository' do
|
|
repository.create_file(User.last, 'foobar', 'CONTENT', message: 'message', branch_name: 'master')
|
|
|
|
# rubocop:disable Style/WordArray
|
|
blobs = repository.blobs_at([['master', 'foobar']])
|
|
# rubocop:enable Style/WordArray
|
|
|
|
expect(blobs.first.name).to eq('foobar')
|
|
expect(blobs.size).to eq(1)
|
|
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
|
|
|
|
it_behaves_like 'asymmetric cached method', :root_ref
|
|
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(: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(:has_visible_content?)
|
|
|
|
repository.expire_emptiness_caches
|
|
end
|
|
|
|
it 'expires the memoized repository cache' do
|
|
allow(repository.raw_repository).to receive(:expire_has_local_branches_cache).and_call_original
|
|
|
|
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' }
|
|
|
|
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
|
|
|
|
def merge(repository, user, merge_request, message)
|
|
repository.merge(user, merge_request.diff_head_sha, merge_request, message)
|
|
end
|
|
end
|
|
|
|
describe '#merge_to_ref' do
|
|
let(:merge_request) do
|
|
create(:merge_request, source_branch: 'feature',
|
|
target_branch: 'master',
|
|
source_project: project)
|
|
end
|
|
|
|
it 'writes merge of source SHA and first parent ref to MR merge_ref_path' do
|
|
merge_commit_id = repository.merge_to_ref(user,
|
|
merge_request.diff_head_sha,
|
|
merge_request,
|
|
merge_request.merge_ref_path,
|
|
'Custom message',
|
|
merge_request.target_branch_ref)
|
|
|
|
merge_commit = repository.commit(merge_commit_id)
|
|
|
|
expect(merge_commit.message).to eq('Custom message')
|
|
expect(merge_commit.author_name).to eq(user.name)
|
|
expect(merge_commit.author_email).to eq(user.commit_email)
|
|
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
|
|
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 '#rebase' do
|
|
let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
|
|
|
|
shared_examples_for 'a method that can rebase successfully' do
|
|
it 'returns the rebase commit sha' do
|
|
rebase_commit_sha = repository.rebase(user, merge_request)
|
|
head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
|
|
|
|
expect(rebase_commit_sha).to eq(head_sha)
|
|
end
|
|
|
|
it 'sets the `rebase_commit_sha` for the given merge request' do
|
|
rebase_commit_sha = repository.rebase(user, merge_request)
|
|
|
|
expect(rebase_commit_sha).not_to be_nil
|
|
expect(merge_request.rebase_commit_sha).to eq(rebase_commit_sha)
|
|
end
|
|
end
|
|
|
|
context 'when two_step_rebase feature is enabled' do
|
|
before do
|
|
stub_feature_flags(two_step_rebase: true)
|
|
end
|
|
|
|
it_behaves_like 'a method that can rebase successfully'
|
|
|
|
it 'executes the new Gitaly RPC' do
|
|
expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rebase)
|
|
expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:user_rebase)
|
|
|
|
repository.rebase(user, merge_request)
|
|
end
|
|
|
|
describe 'rolling back the `rebase_commit_sha`' do
|
|
let(:new_sha) { Digest::SHA1.hexdigest('foo') }
|
|
|
|
it 'does not rollback when there are no errors' do
|
|
second_response = double(pre_receive_error: nil, git_error: nil)
|
|
mock_gitaly(second_response)
|
|
|
|
repository.rebase(user, merge_request)
|
|
|
|
expect(merge_request.reload.rebase_commit_sha).to eq(new_sha)
|
|
end
|
|
|
|
it 'does rollback when an error is encountered in the second step' do
|
|
second_response = double(pre_receive_error: 'my_error', git_error: nil)
|
|
mock_gitaly(second_response)
|
|
|
|
expect do
|
|
repository.rebase(user, merge_request)
|
|
end.to raise_error(Gitlab::Git::PreReceiveError)
|
|
|
|
expect(merge_request.reload.rebase_commit_sha).to be_nil
|
|
end
|
|
|
|
def mock_gitaly(second_response)
|
|
responses = [
|
|
double(rebase_sha: new_sha).as_null_object,
|
|
second_response
|
|
]
|
|
|
|
expect_any_instance_of(
|
|
Gitaly::OperationService::Stub
|
|
).to receive(:user_rebase_confirmable).and_return(responses.each)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when two_step_rebase feature is disabled' do
|
|
before do
|
|
stub_feature_flags(two_step_rebase: false)
|
|
end
|
|
|
|
it_behaves_like 'a method that can rebase successfully'
|
|
|
|
it 'executes the deprecated Gitaly RPC' do
|
|
expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:user_rebase)
|
|
expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:rebase)
|
|
|
|
repository.rebase(user, merge_request)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#revert' 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
|
|
|
|
describe '#cherry_pick' 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
|
|
|
|
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 method caches' do
|
|
expect(repository).to receive(:expire_method_caches).with([
|
|
:size,
|
|
:commit_count,
|
|
:rendered_readme,
|
|
:readme_path,
|
|
:contribution_guide,
|
|
:changelog,
|
|
:license_blob,
|
|
:license_key,
|
|
:gitignore,
|
|
:gitlab_ci_yml,
|
|
:branch_names,
|
|
:tag_names,
|
|
:branch_count,
|
|
:tag_count,
|
|
:avatar,
|
|
:exists?,
|
|
:root_ref,
|
|
:has_visible_content?,
|
|
:issue_template_names,
|
|
:merge_request_template_names,
|
|
:metrics_dashboard_paths,
|
|
:xcode_project?
|
|
])
|
|
|
|
repository.after_change_head
|
|
end
|
|
end
|
|
|
|
describe '#expires_caches_for_tags' 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.expire_caches_for_tags
|
|
end
|
|
end
|
|
|
|
describe '#before_push_tag' do
|
|
it 'logs an event' do
|
|
expect(repository).not_to receive(:expire_statistics_caches)
|
|
expect(repository).not_to receive(:expire_emptiness_caches)
|
|
expect(repository).not_to receive(:expire_tags_cache)
|
|
expect(repository).to receive(:repository_event).with(:push_tag)
|
|
|
|
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
|
|
|
|
it 'does not expire the branch caches when specified' do
|
|
expect(repository).not_to receive(:expire_branches_cache)
|
|
|
|
repository.after_create_branch(expire_cache: false)
|
|
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
|
|
|
|
it 'does not expire the branch caches when specified' do
|
|
expect(repository).not_to receive(:expire_branches_cache)
|
|
|
|
repository.after_remove_branch(expire_cache: false)
|
|
end
|
|
end
|
|
|
|
describe '#after_create' do
|
|
it 'calls expire_status_cache' do
|
|
expect(repository).to receive(:expire_status_cache)
|
|
|
|
repository.after_create
|
|
end
|
|
|
|
it 'logs an event' do
|
|
expect(repository).to receive(:repository_event).with(:create_repository)
|
|
|
|
repository.after_create
|
|
end
|
|
end
|
|
|
|
describe '#expire_status_cache' do
|
|
it 'flushes the exists cache' do
|
|
expect(repository).to receive(:expire_exists_cache)
|
|
|
|
repository.expire_status_cache
|
|
end
|
|
|
|
it 'flushes the root ref cache' do
|
|
expect(repository).to receive(:expire_root_ref_cache)
|
|
|
|
repository.expire_status_cache
|
|
end
|
|
|
|
it 'flushes the emptiness caches' do
|
|
expect(repository).to receive(:expire_emptiness_caches)
|
|
|
|
repository.expire_status_cache
|
|
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)
|
|
|
|
rugged_count = rugged_repo(repository).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)
|
|
|
|
rugged_count = rugged_repo(repository).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) }
|
|
|
|
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
|
|
|
|
describe '#rm_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
|
|
|
|
context 'when pre hooks failed' do
|
|
before do
|
|
allow_any_instance_of(Gitlab::GitalyClient::OperationService)
|
|
.to receive(:user_delete_branch).and_raise(Gitlab::Git::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::PreReceiveError)
|
|
|
|
expect(repository.find_branch('feature')).not_to be_nil
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#rm_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
|
|
|
|
describe '#avatar' do
|
|
it 'returns nil if repo does not exist' do
|
|
allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
|
|
|
|
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) }
|
|
let(:request_store_cache) { repository.send(:request_store_cache) }
|
|
|
|
it 'expires the cache' do
|
|
expect(cache).to receive(:expire).with(:exists?)
|
|
|
|
repository.expire_exists_cache
|
|
end
|
|
|
|
it 'expires the request store cache', :request_store do
|
|
expect(request_store_cache).to receive(:expire).with(:exists?)
|
|
|
|
repository.expire_exists_cache
|
|
end
|
|
end
|
|
|
|
describe '#xcode_project?' do
|
|
before do
|
|
allow(repository).to receive(:tree).with(:head).and_return(double(:tree, trees: [tree]))
|
|
end
|
|
|
|
context 'when the root contains a *.xcodeproj directory' do
|
|
let(:tree) { double(:tree, path: 'Foo.xcodeproj') }
|
|
|
|
it 'returns true' do
|
|
expect(repository.xcode_project?).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'when the root contains a *.xcworkspace directory' do
|
|
let(:tree) { double(:tree, path: 'Foo.xcworkspace') }
|
|
|
|
it 'returns true' do
|
|
expect(repository.xcode_project?).to be_truthy
|
|
end
|
|
end
|
|
|
|
context 'when the root contains no Xcode config directory' do
|
|
let(:tree) { double(:tree, path: 'Foo') }
|
|
|
|
it 'returns false' do
|
|
expect(repository.xcode_project?).to be_falsey
|
|
end
|
|
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 = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
|
|
File.join(repository.path, ref)
|
|
end
|
|
# 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
|
|
|
|
context 'for multiple SHAs' do
|
|
it 'skips non-existent SHAs' do
|
|
repository.keep_around('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', sample_commit.id)
|
|
|
|
expect(repository.kept_around?(sample_commit.id)).to be_truthy
|
|
end
|
|
|
|
it 'skips already-kept-around SHAs' do
|
|
repository.keep_around(sample_commit.id)
|
|
|
|
expect(repository.raw_repository).to receive(:write_ref).exactly(1).and_call_original
|
|
|
|
repository.keep_around(sample_commit.id, another_sample_commit.id)
|
|
|
|
expect(repository.kept_around?(another_sample_commit.id)).to be_truthy
|
|
end
|
|
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 '#readme', :use_clean_rails_memory_store_caching do
|
|
context 'with a non-existing repository' do
|
|
let(:project) { create(:project) }
|
|
|
|
it 'returns nil' do
|
|
expect(repository.readme).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
context 'when no README exists' do
|
|
let(:project) { create(:project, :empty_repo) }
|
|
|
|
it 'returns nil' do
|
|
expect(repository.readme).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when a README exists' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
it 'returns the README' do
|
|
expect(repository.readme).to be_an_instance_of(ReadmeBlob)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#readme_path', :use_clean_rails_memory_store_caching do
|
|
context 'with a non-existing repository' do
|
|
let(:project) { create(:project) }
|
|
|
|
it 'returns nil' do
|
|
expect(repository.readme_path).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'with an existing repository' do
|
|
context 'when no README exists' do
|
|
let(:project) { create(:project, :empty_repo) }
|
|
|
|
it 'returns nil' do
|
|
expect(repository.readme_path).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when a README exists' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
it 'returns the README' do
|
|
expect(repository.readme_path).to eq("README.md")
|
|
end
|
|
|
|
it 'caches the response' do
|
|
expect(repository).to receive(:readme).and_call_original.once
|
|
|
|
2.times do
|
|
expect(repository.readme_path).to eq("README.md")
|
|
end
|
|
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_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(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS).to include(*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 '#local_branches' do
|
|
it 'returns the local branches' do
|
|
masterrev = repository.find_branch('master').dereferenced_target
|
|
create_remote_branch('joe', 'remote_branch', masterrev)
|
|
repository.add_branch(user, 'local_branch', masterrev.id)
|
|
|
|
expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
|
|
expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
|
|
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 '#refresh_method_caches' do
|
|
it 'refreshes the caches of the given types' do
|
|
expect(repository).to receive(:expire_method_caches)
|
|
.with(%i(rendered_readme readme_path license_blob license_key license))
|
|
|
|
expect(repository).to receive(:rendered_readme)
|
|
expect(repository).to receive(:readme_path)
|
|
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
|
|
|
|
def create_remote_branch(remote_name, branch_name, target)
|
|
rugged = rugged_repo(repository)
|
|
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
|
|
end
|
|
|
|
shared_examples '#ancestor?' do
|
|
let(:commit) { repository.commit }
|
|
let(:ancestor) { commit.parents.first }
|
|
|
|
it 'is an ancestor' do
|
|
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
|
|
end
|
|
|
|
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
|
|
|
|
it 'returns false for invalid commit IDs' do
|
|
expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
|
|
expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
|
|
end
|
|
end
|
|
|
|
describe '#ancestor? with Gitaly enabled' do
|
|
it_behaves_like "#ancestor?"
|
|
end
|
|
|
|
describe '#ancestor? with Rugged enabled', :enable_rugged do
|
|
it 'calls out to the Rugged implementation' do
|
|
allow_any_instance_of(Rugged).to receive(:merge_base).with(repository.commit.id, Gitlab::Git::BLANK_SHA).and_call_original
|
|
|
|
repository.ancestor?(repository.commit.id, Gitlab::Git::BLANK_SHA)
|
|
end
|
|
|
|
it_behaves_like '#ancestor?'
|
|
end
|
|
|
|
describe '#archive_metadata' do
|
|
let(:ref) { 'master' }
|
|
let(:storage_path) { '/tmp' }
|
|
|
|
let(:prefix) { [project.path, ref].join('-') }
|
|
let(:filename) { prefix + '.tar.gz' }
|
|
|
|
subject(:result) { repository.archive_metadata(ref, storage_path, append_sha: false) }
|
|
|
|
context 'with hashed storage disabled' do
|
|
let(:project) { create(:project, :repository, :legacy_storage) }
|
|
|
|
it 'uses the project path to generate the filename' do
|
|
expect(result['ArchivePrefix']).to eq(prefix)
|
|
expect(File.basename(result['ArchivePath'])).to eq(filename)
|
|
end
|
|
end
|
|
|
|
context 'with hashed storage enabled' do
|
|
it 'uses the project path to generate the filename' do
|
|
expect(result['ArchivePrefix']).to eq(prefix)
|
|
expect(File.basename(result['ArchivePath'])).to eq(filename)
|
|
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).to receive(:find).once
|
|
|
|
2.times { project.commit_by(oid: oid) }
|
|
end
|
|
|
|
it 'caches nil values' do
|
|
expect(Gitlab::Git::Commit).to receive(:find).once
|
|
|
|
2.times { 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}")
|
|
expect(subject.gl_project_path).to eq(project.full_path)
|
|
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}")
|
|
expect(subject.gl_project_path).to eq(project.full_path)
|
|
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
|
|
|
|
describe '#merge_base' do
|
|
set(:project) { create(:project, :repository) }
|
|
subject(:repository) { project.repository }
|
|
|
|
it 'only makes one gitaly call' do
|
|
expect(Gitlab::GitalyClient).to receive(:call).once.and_call_original
|
|
|
|
repository.merge_base('master', 'fix')
|
|
end
|
|
end
|
|
|
|
describe '#create_if_not_exists' do
|
|
let(:project) { create(:project) }
|
|
let(:repository) { project.repository }
|
|
|
|
it 'creates the repository if it did not exist' do
|
|
expect { repository.create_if_not_exists }.to change { repository.exists? }.from(false).to(true)
|
|
end
|
|
|
|
it 'calls out to the repository client to create a repo' do
|
|
expect(repository.raw.gitaly_repository_client).to receive(:create_repository)
|
|
|
|
repository.create_if_not_exists
|
|
end
|
|
|
|
context 'it does nothing if the repository already existed' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
it 'does nothing if the repository already existed' do
|
|
expect(repository.raw.gitaly_repository_client).not_to receive(:create_repository)
|
|
|
|
repository.create_if_not_exists
|
|
end
|
|
end
|
|
|
|
context 'when the repository exists but the cache is not up to date' do
|
|
let(:project) { create(:project, :repository) }
|
|
|
|
it 'does not raise errors' do
|
|
allow(repository).to receive(:exists?).and_return(false)
|
|
expect(repository.raw).to receive(:create_repository).and_call_original
|
|
|
|
expect { repository.create_if_not_exists }.not_to raise_error
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "#blobs_metadata" do
|
|
set(:project) { create(:project, :repository) }
|
|
let(:repository) { project.repository }
|
|
|
|
def expect_metadata_blob(thing)
|
|
expect(thing).to be_a(Blob)
|
|
expect(thing.data).to be_empty
|
|
end
|
|
|
|
it "returns blob metadata in batch for HEAD" do
|
|
result = repository.blobs_metadata(["bar/branch-test.txt", "README.md", "does/not/exist"])
|
|
|
|
expect_metadata_blob(result.first)
|
|
expect_metadata_blob(result.second)
|
|
expect(result.size).to eq(2)
|
|
end
|
|
|
|
it "returns blob metadata for a specified ref" do
|
|
result = repository.blobs_metadata(["files/ruby/feature.rb"], "feature")
|
|
|
|
expect_metadata_blob(result.first)
|
|
end
|
|
|
|
it "performs a single gitaly call", :request_store do
|
|
expect { repository.blobs_metadata(["bar/branch-test.txt", "readme.txt", "does/not/exist"]) }
|
|
.to change { Gitlab::GitalyClient.get_request_count }.by(1)
|
|
end
|
|
end
|
|
end
|