diff options
author | blackst0ne <blackst0ne.ru@gmail.com> | 2017-08-18 14:26:11 +1100 |
---|---|---|
committer | blackst0ne <blackst0ne.ru@gmail.com> | 2017-08-18 14:26:11 +1100 |
commit | 2047991363844b4f17216f94778b432ed198a412 (patch) | |
tree | 6b90710c44dae00178ad323d796beb59737f47f1 /spec/lib/gitlab/git | |
parent | 7a1c5ba7471b233c994f5b5b313085d0d1292b5d (diff) | |
parent | f3203cbbc25586df622552153cc460d2f79f414e (diff) | |
download | gitlab-ce-2047991363844b4f17216f94778b432ed198a412.tar.gz |
Merge remote-tracking branch 'upstream/master'
Diffstat (limited to 'spec/lib/gitlab/git')
-rw-r--r-- | spec/lib/gitlab/git/attributes_spec.rb | 44 | ||||
-rw-r--r-- | spec/lib/gitlab/git/blame_spec.rb | 99 | ||||
-rw-r--r-- | spec/lib/gitlab/git/blob_spec.rb | 111 | ||||
-rw-r--r-- | spec/lib/gitlab/git/commit_spec.rb | 206 | ||||
-rw-r--r-- | spec/lib/gitlab/git/compare_spec.rb | 4 | ||||
-rw-r--r-- | spec/lib/gitlab/git/diff_collection_spec.rb | 91 | ||||
-rw-r--r-- | spec/lib/gitlab/git/diff_spec.rb | 94 | ||||
-rw-r--r-- | spec/lib/gitlab/git/encoding_helper_spec.rb | 88 | ||||
-rw-r--r-- | spec/lib/gitlab/git/gitmodules_parser_spec.rb | 28 | ||||
-rw-r--r-- | spec/lib/gitlab/git/hook_spec.rb | 46 | ||||
-rw-r--r-- | spec/lib/gitlab/git/repository_spec.rb | 675 | ||||
-rw-r--r-- | spec/lib/gitlab/git/rev_list_spec.rb | 6 | ||||
-rw-r--r-- | spec/lib/gitlab/git/storage/circuit_breaker_spec.rb | 329 | ||||
-rw-r--r-- | spec/lib/gitlab/git/storage/forked_storage_check_spec.rb | 58 | ||||
-rw-r--r-- | spec/lib/gitlab/git/storage/health_spec.rb | 87 | ||||
-rw-r--r-- | spec/lib/gitlab/git/tag_spec.rb | 38 | ||||
-rw-r--r-- | spec/lib/gitlab/git/tree_spec.rb | 23 | ||||
-rw-r--r-- | spec/lib/gitlab/git/util_spec.rb | 4 |
18 files changed, 1336 insertions, 695 deletions
diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb index 1cfd8db09a5..b715fc3410a 100644 --- a/spec/lib/gitlab/git/attributes_spec.rb +++ b/spec/lib/gitlab/git/attributes_spec.rb @@ -14,13 +14,13 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'returns a Hash containing multiple attributes' do - expect(subject.attributes('test.sh')). - to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' }) + expect(subject.attributes('test.sh')) + .to eq({ 'eol' => 'lf', 'gitlab-language' => 'shell' }) end it 'returns a Hash containing attributes for a file with multiple extensions' do - expect(subject.attributes('test.haml.html')). - to eq({ 'gitlab-language' => 'haml' }) + expect(subject.attributes('test.haml.html')) + .to eq({ 'gitlab-language' => 'haml' }) end it 'returns a Hash containing attributes for a file in a directory' do @@ -28,8 +28,8 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'returns a Hash containing attributes with query string parameters' do - expect(subject.attributes('foo.cgi')). - to eq({ 'key' => 'value?p1=v1&p2=v2' }) + expect(subject.attributes('foo.cgi')) + .to eq({ 'key' => 'value?p1=v1&p2=v2' }) end it 'returns a Hash containing the attributes for an absolute path' do @@ -39,11 +39,11 @@ describe Gitlab::Git::Attributes, seed_helper: true do it 'returns a Hash containing the attributes when a pattern is defined using an absolute path' do # When a path is given without a leading slash it should still match # patterns defined with a leading slash. - expect(subject.attributes('foo.png')). - to eq({ 'gitlab-language' => 'png' }) + expect(subject.attributes('foo.png')) + .to eq({ 'gitlab-language' => 'png' }) - expect(subject.attributes('/foo.png')). - to eq({ 'gitlab-language' => 'png' }) + expect(subject.attributes('/foo.png')) + .to eq({ 'gitlab-language' => 'png' }) end it 'returns an empty Hash for a defined path without attributes' do @@ -74,8 +74,8 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'parses an entry that uses a tab to separate the pattern and attributes' do - expect(subject.patterns[File.join(path, '*.md')]). - to eq({ 'gitlab-language' => 'markdown' }) + expect(subject.patterns[File.join(path, '*.md')]) + .to eq({ 'gitlab-language' => 'markdown' }) end it 'stores patterns in reverse order' do @@ -91,9 +91,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'does not parse anything when the attributes file does not exist' do - expect(File).to receive(:exist?). - with(File.join(path, 'info/attributes')). - and_return(false) + expect(File).to receive(:exist?) + .with(File.join(path, 'info/attributes')) + .and_return(false) expect(subject.patterns).to eq({}) end @@ -115,13 +115,13 @@ describe Gitlab::Git::Attributes, seed_helper: true do it 'parses multiple attributes' do input = 'boolean key=value -negated' - expect(subject.parse_attributes(input)). - to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false }) + expect(subject.parse_attributes(input)) + .to eq({ 'boolean' => true, 'key' => 'value', 'negated' => false }) end it 'parses attributes with query string parameters' do - expect(subject.parse_attributes('foo=bar?baz=1')). - to eq({ 'foo' => 'bar?baz=1' }) + expect(subject.parse_attributes('foo=bar?baz=1')) + .to eq({ 'foo' => 'bar?baz=1' }) end end @@ -133,9 +133,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do end it 'does not yield when the attributes file does not exist' do - expect(File).to receive(:exist?). - with(File.join(path, 'info/attributes')). - and_return(false) + expect(File).to receive(:exist?) + .with(File.join(path, 'info/attributes')) + .and_return(false) expect { |b| subject.each_line(&b) }.not_to yield_control end diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 8b041ac69b1..800c245b130 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -7,60 +7,73 @@ describe Gitlab::Git::Blame, seed_helper: true do Gitlab::Git::Blame.new(repository, SeedRepo::Commit::ID, "CONTRIBUTING.md") end - context "each count" do - it do - data = [] - blame.each do |commit, line| - data << { - commit: commit, - line: line - } - end - - expect(data.size).to eq(95) - expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(data.first[:line]).to eq("# Contribute to GitLab") - end - end + shared_examples 'blaming a file' do + context "each count" do + it do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end - context "ISO-8859 encoding" do - let(:blame) do - Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + expect(data.size).to eq(95) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("# Contribute to GitLab") + expect(data.first[:line]).to be_utf8 + end end - it 'converts to UTF-8' do - data = [] - blame.each do |commit, line| - data << { - commit: commit, - line: line - } + context "ISO-8859 encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") end - expect(data.size).to eq(1) - expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(data.first[:line]).to eq("Ä ü") - end - end + it 'converts to UTF-8' do + data = [] + blame.each do |commit, line| + data << { + commit: commit, + line: line + } + end - context "unknown encoding" do - let(:blame) do - Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq("Ä ü") + expect(data.first[:line]).to be_utf8 + end end - it 'converts to UTF-8' do - expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil) - data = [] - blame.each do |commit, line| - data << { + context "unknown encoding" do + let(:blame) do + Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt") + end + + it 'converts to UTF-8' do + expect(CharlockHolmes::EncodingDetector).to receive(:detect).and_return(nil) + data = [] + blame.each do |commit, line| + data << { commit: commit, line: line - } - end + } + end - expect(data.size).to eq(1) - expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) - expect(data.first[:line]).to eq(" ") + expect(data.size).to eq(1) + expect(data.first[:commit]).to be_kind_of(Gitlab::Git::Commit) + expect(data.first[:line]).to eq(" ") + expect(data.first[:line]).to be_utf8 + end end end + + context 'when Gitaly blame feature is enabled' do + it_behaves_like 'blaming a file' + end + + context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'blaming a file' + end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index e6a07a58d73..dfab0c2fe85 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::Git::Blob, seed_helper: true do end end - describe '.find' do + shared_examples 'finding blobs' do context 'file in subdir' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") } @@ -78,12 +78,18 @@ describe Gitlab::Git::Blob, seed_helper: true do context 'large file' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') } let(:blob_size) { 111803 } + let(:stub_limit) { 1000 } + + before do + stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', stub_limit) + end it { expect(blob.size).to eq(blob_size) } - it { expect(blob.data.length).to eq(blob_size) } + it { expect(blob.data.length).to eq(stub_limit) } it 'check that this test is sane' do - expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + # It only makes sense to test limiting if the blob is larger than the limit. + expect(blob.size).to be > Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE end it 'can load all data' do @@ -92,16 +98,26 @@ describe Gitlab::Git::Blob, seed_helper: true do end it 'marks the blob as binary' do - expect(Gitlab::Git::Blob).to receive(:new). - with(hash_including(binary: true)). - and_call_original + expect(Gitlab::Git::Blob).to receive(:new) + .with(hash_including(binary: true)) + .and_call_original expect(blob).to be_binary end end end - describe '.raw' do + describe '.find' do + context 'when project_raw_show Gitaly feature is enabled' do + it_behaves_like 'finding blobs' + end + + context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding blobs' + end + end + + shared_examples 'finding blobs by ID' do let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) } it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) } it { expect(raw_blob.data[0..10]).to eq("require \'fi") } @@ -126,6 +142,87 @@ describe Gitlab::Git::Blob, seed_helper: true do end end + describe '.raw' do + context 'when the blob_raw Gitaly feature is enabled' do + it_behaves_like 'finding blobs by ID' + end + + context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding blobs by ID' + end + end + + describe '.batch' do + let(:blob_references) do + [ + [SeedRepo::Commit::ID, "files/ruby/popen.rb"], + [SeedRepo::Commit::ID, 'six'] + ] + end + + subject { described_class.batch(repository, blob_references) } + + it { expect(subject.size).to eq(blob_references.size) } + + context 'first blob' do + let(:blob) { subject[0] } + + it { expect(blob.id).to eq(SeedRepo::RubyBlob::ID) } + it { expect(blob.name).to eq(SeedRepo::RubyBlob::NAME) } + it { expect(blob.path).to eq("files/ruby/popen.rb") } + it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) } + it { expect(blob.data[0..10]).to eq(SeedRepo::RubyBlob::CONTENT[0..10]) } + it { expect(blob.size).to eq(669) } + it { expect(blob.mode).to eq("100644") } + end + + context 'second blob' do + let(:blob) { subject[1] } + + it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') } + it { expect(blob.data).to eq('') } + it 'does not mark the blob as binary' do + expect(blob).not_to be_binary + end + end + + context 'limiting' do + subject { described_class.batch(repository, blob_references, blob_size_limit: blob_size_limit) } + + context 'default' do + let(:blob_size_limit) { nil } + + it 'limits to MAX_DATA_DISPLAY_SIZE' do + stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) + + expect(subject.first.data.size).to eq(100) + end + end + + context 'positive' do + let(:blob_size_limit) { 10 } + + it { expect(subject.first.data.size).to eq(10) } + end + + context 'zero' do + let(:blob_size_limit) { 0 } + + it { expect(subject.first.data).to eq('') } + end + + context 'negative' do + let(:blob_size_limit) { -1 } + + it 'ignores MAX_DATA_DISPLAY_SIZE' do + stub_const('Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE', 100) + + expect(subject.first.data.size).to eq(669) + end + end + end + end + describe 'encoding' do context 'file with russian text' do let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") } diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 3e44c577643..ac33cd8a2c9 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -2,7 +2,7 @@ require "spec_helper" describe Gitlab::Git::Commit, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } - let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) } + let(:commit) { described_class.find(repository, SeedRepo::Commit::ID) } let(:rugged_commit) do repository.rugged.lookup(SeedRepo::Commit::ID) end @@ -24,7 +24,7 @@ describe Gitlab::Git::Commit, seed_helper: true do } @parents = [repo.head.target] - @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) } + @gitlab_parents = @parents.map { |c| described_class.decorate(repository, c) } @tree = @parents.first.tree sha = Rugged::Commit.create( @@ -38,7 +38,7 @@ describe Gitlab::Git::Commit, seed_helper: true do ) @raw_commit = repo.lookup(sha) - @commit = Gitlab::Git::Commit.new(@raw_commit) + @commit = described_class.new(repository, @raw_commit) end it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) } @@ -64,48 +64,97 @@ describe Gitlab::Git::Commit, seed_helper: true do end end + describe "Commit info from gitaly commit" do + let(:id) { 'f00' } + let(:parent_ids) { %w(b45 b46) } + let(:subject) { "My commit".force_encoding('ASCII-8BIT') } + let(:body) { subject + "My body".force_encoding('ASCII-8BIT') } + let(:committer) do + Gitaly::CommitAuthor.new( + name: generate(:name), + email: generate(:email), + date: Google::Protobuf::Timestamp.new(seconds: 123) + ) + end + let(:author) do + Gitaly::CommitAuthor.new( + name: generate(:name), + email: generate(:email), + date: Google::Protobuf::Timestamp.new(seconds: 456) + ) + end + let(:gitaly_commit) do + Gitaly::GitCommit.new( + id: id, + subject: subject, + body: body, + author: author, + committer: committer, + parent_ids: parent_ids + ) + end + let(:commit) { described_class.new(repository, gitaly_commit) } + + it { expect(commit.short_id).to eq(id[0..10]) } + it { expect(commit.id).to eq(id) } + it { expect(commit.sha).to eq(id) } + it { expect(commit.safe_message).to eq(body) } + it { expect(commit.created_at).to eq(Time.at(committer.date.seconds)) } + it { expect(commit.author_email).to eq(author.email) } + it { expect(commit.author_name).to eq(author.name) } + it { expect(commit.committer_name).to eq(committer.name) } + it { expect(commit.committer_email).to eq(committer.email) } + it { expect(commit.parent_ids).to eq(parent_ids) } + + context 'no body' do + let(:body) { "".force_encoding('ASCII-8BIT') } + + it { expect(commit.safe_message).to eq(subject) } + end + end + context 'Class methods' do describe '.find' do it "should return first head commit if without params" do - expect(Gitlab::Git::Commit.last(repository).id).to eq( - repository.raw.head.target.oid + expect(described_class.last(repository).id).to eq( + repository.rugged.head.target.oid ) end it "should return valid commit" do - expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_valid_commit + expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit end it "should return valid commit for tag" do - expect(Gitlab::Git::Commit.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') + expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') end it "should return nil for non-commit ids" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") - expect(Gitlab::Git::Commit.find(repository, blob.id)).to be_nil + expect(described_class.find(repository, blob.id)).to be_nil end it "should return nil for parent of non-commit object" do blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") - expect(Gitlab::Git::Commit.find(repository, "#{blob.id}^")).to be_nil + expect(described_class.find(repository, "#{blob.id}^")).to be_nil end it "should return nil for nonexisting ids" do - expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil + expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil end context 'with broken repo' do let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH) } it 'returns nil' do - expect(Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID)).to be_nil + expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_nil end end end describe '.last_for_path' do context 'no path' do - subject { Gitlab::Git::Commit.last_for_path(repository, 'master') } + subject { described_class.last_for_path(repository, 'master') } describe '#id' do subject { super().id } @@ -114,7 +163,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'path' do - subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') } + subject { described_class.last_for_path(repository, 'master', 'files/ruby') } describe '#id' do subject { super().id } @@ -123,7 +172,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end context 'ref + path' do - subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } + subject { described_class.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') } describe '#id' do subject { super().id } @@ -135,7 +184,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.where' do context 'path is empty string' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'master', path: '', @@ -153,7 +202,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'path is nil' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'master', path: nil, @@ -171,7 +220,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref is branch name' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'master', path: 'files', @@ -191,7 +240,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref is commit id' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e", path: 'files', @@ -211,7 +260,7 @@ describe Gitlab::Git::Commit, seed_helper: true do context 'ref is tag' do subject do - commits = Gitlab::Git::Commit.where( + commits = described_class.where( repo: repository, ref: 'v1.0.0', path: 'files', @@ -232,7 +281,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.between' do subject do - commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) + commits = described_class.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID) commits.map { |c| c.id } end @@ -244,68 +293,91 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '.find_all' do - context 'max_count' do - subject do - commits = Gitlab::Git::Commit.find_all( - repository, - max_count: 50 - ) + shared_examples 'finding all commits' do + it 'should return a return a collection of commits' do + commits = described_class.find_all(repository) - commits.map { |c| c.id } + expect(commits).to all( be_a_kind_of(described_class) ) end - it 'has 31 elements' do - expect(subject.size).to eq(33) + context 'max_count' do + subject do + commits = described_class.find_all( + repository, + max_count: 50 + ) + + commits.map(&:id) + end + + it 'has 34 elements' do + expect(subject.size).to eq(34) + end + + it 'includes the expected commits' do + expect(subject).to include( + SeedRepo::Commit::ID, + SeedRepo::Commit::PARENT_ID, + SeedRepo::FirstCommit::ID + ) + end end - it { is_expected.to include(SeedRepo::Commit::ID) } - it { is_expected.to include(SeedRepo::Commit::PARENT_ID) } - it { is_expected.to include(SeedRepo::FirstCommit::ID) } - end - - context 'ref + max_count + skip' do - subject do - commits = Gitlab::Git::Commit.find_all( - repository, - ref: 'master', - max_count: 50, - skip: 1 - ) - commits.map { |c| c.id } + context 'ref + max_count + skip' do + subject do + commits = described_class.find_all( + repository, + ref: 'master', + max_count: 50, + skip: 1 + ) + + commits.map(&:id) + end + + it 'has 24 elements' do + expect(subject.size).to eq(24) + end + + it 'includes the expected commits' do + expect(subject).to include(SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID) + expect(subject).not_to include(SeedRepo::LastCommit::ID) + end end + end - it 'has 23 elements' do - expect(subject.size).to eq(24) - end - it { is_expected.to include(SeedRepo::Commit::ID) } - it { is_expected.to include(SeedRepo::FirstCommit::ID) } - it { is_expected.not_to include(SeedRepo::LastCommit::ID) } + context 'when Gitaly find_all_commits feature is enabled' do + it_behaves_like 'finding all commits' end - context 'contains feature + max_count' do - subject do - commits = Gitlab::Git::Commit.find_all( - repository, - contains: 'feature', - max_count: 7 - ) + context 'when Gitaly find_all_commits feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'finding all commits' - commits.map { |c| c.id } - end + context 'while applying a sort order based on the `order` option' do + it "allows ordering topologically (no parents shown before their children)" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO) - it 'has 7 elements' do - expect(subject.size).to eq(7) - end + described_class.find_all(repository, order: :topo) + end - it { is_expected.not_to include(SeedRepo::Commit::PARENT_ID) } - it { is_expected.not_to include(SeedRepo::Commit::ID) } - it { is_expected.to include(SeedRepo::BigCommit::ID) } + it "allows ordering by date" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE | Rugged::SORT_TOPO) + + described_class.find_all(repository, order: :date) + end + + it "applies no sorting by default" do + expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE) + + described_class.find_all(repository) + end + end end end end describe '#init_from_rugged' do - let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) } + let(:gitlab_commit) { described_class.new(repository, rugged_commit) } subject { gitlab_commit } describe '#id' do @@ -315,7 +387,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '#init_from_hash' do - let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) } + let(:commit) { described_class.new(repository, sample_commit_hash) } subject { commit } describe '#id' do @@ -382,7 +454,7 @@ describe Gitlab::Git::Commit, seed_helper: true do end describe '#ref_names' do - let(:commit) { Gitlab::Git::Commit.find(repository, 'master') } + let(:commit) { described_class.find(repository, 'master') } subject { commit.ref_names(repository) } it 'has 1 element' do diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb index 7c45071ec45..4c9f4a28f32 100644 --- a/spec/lib/gitlab/git/compare_spec.rb +++ b/spec/lib/gitlab/git/compare_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" describe Gitlab::Git::Compare, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } - let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, false) } - let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, true) } + let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: false) } + let(:compare_straight) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::Commit::ID, straight: true) } describe '#commits' do subject do diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb index 122c93dcd69..3494f0cc98d 100644 --- a/spec/lib/gitlab/git/diff_collection_spec.rb +++ b/spec/lib/gitlab/git/diff_collection_spec.rb @@ -6,18 +6,18 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do iterator, max_files: max_files, max_lines: max_lines, - all_diffs: all_diffs, - no_collapse: no_collapse + limits: limits, + expanded: expanded ) end - let(:iterator) { Array.new(file_count, fake_diff(line_length, line_count)) } + let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) } let(:file_count) { 0 } let(:line_length) { 1 } let(:line_count) { 1 } let(:max_files) { 10 } let(:max_lines) { 100 } - let(:all_diffs) { false } - let(:no_collapse) { true } + let(:limits) { true } + let(:expanded) { true } describe '#to_a' do subject { super().to_a } @@ -64,10 +64,18 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do subject { super().real_size } it { is_expected.to eq('3') } end - it { expect(subject.size).to eq(3) } + + describe '#size' do + it { expect(subject.size).to eq(3) } + + it 'does not change after peeking' do + subject.any? + expect(subject.size).to eq(3) + end + end context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -83,7 +91,15 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do subject { super().real_size } it { is_expected.to eq('3') } end - it { expect(subject.size).to eq(3) } + + describe '#size' do + it { expect(subject.size).to eq(3) } + + it 'does not change after peeking' do + subject.any? + expect(subject.size).to eq(3) + end + end end end @@ -107,7 +123,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(0) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -151,7 +167,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(10) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -191,7 +207,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(3) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -257,7 +273,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do it { expect(subject.size).to eq(9) } context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } describe '#overflow?' do subject { super().overflow? } @@ -309,8 +325,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end it 'yields Diff instances even when they are too large' do - expect { |b| collection.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| collection.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'prunes diffs that are too large' do @@ -325,14 +341,15 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when diff is quite large will collapse by default' do - let(:iterator) { [{ diff: 'a' * 20480 }] } + let(:iterator) { [{ diff: 'a' * (Gitlab::Git::Diff.collapse_limit + 1) }] } + let(:max_files) { 100 } context 'when no collapse is set' do - let(:no_collapse) { true } + let(:expanded) { true } it 'yields Diff instances even when they are quite big' do - expect { |b| subject.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| subject.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'does not prune diffs' do @@ -347,11 +364,11 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when no collapse is unset' do - let(:no_collapse) { false } + let(:expanded) { false } it 'yields Diff instances even when they are quite big' do - expect { |b| subject.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| subject.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'prunes diffs that are quite big' do @@ -367,7 +384,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do context 'when go over safe limits on files' do let(:iterator) { [fake_diff(1, 1)] * 4 } - before(:each) do + before do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines }) end @@ -392,7 +409,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do ] end - before(:each) do + before do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) end @@ -417,7 +434,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do ] end - before(:each) do + before do stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 }) end @@ -434,11 +451,11 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do end context 'when limiting is disabled' do - let(:all_diffs) { true } + let(:limits) { false } it 'yields Diff instances even when they are quite big' do - expect { |b| subject.each(&b) }. - to yield_with_args(an_instance_of(Gitlab::Git::Diff)) + expect { |b| subject.each(&b) } + .to yield_with_args(an_instance_of(Gitlab::Git::Diff)) end it 'does not prune diffs' do @@ -457,4 +474,24 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do def fake_diff(line_length, line_count) { 'diff' => "#{'a' * line_length}\n" * line_count } end + + class MutatingConstantIterator + include Enumerable + + def initialize(count, value) + @count = count + @value = value + end + + def each + return enum_for(:each) unless block_given? + + loop do + break if @count.zero? + # It is critical to decrement before yielding. We may never reach the lines after 'yield'. + @count -= 1 + yield @value + end + end + end end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 7253a2edeff..7ea3386ac2a 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -31,6 +31,36 @@ EOT [".gitmodules"]).patches.first end + describe 'size limit feature toggles' do + context 'when the feature gitlab_git_diff_size_limit_increase is enabled' do + before do + stub_feature_flags(gitlab_git_diff_size_limit_increase: true) + end + + it 'returns 200 KB for size_limit' do + expect(described_class.size_limit).to eq(200.kilobytes) + end + + it 'returns 100 KB for collapse_limit' do + expect(described_class.collapse_limit).to eq(100.kilobytes) + end + end + + context 'when the feature gitlab_git_diff_size_limit_increase is disabled' do + before do + stub_feature_flags(gitlab_git_diff_size_limit_increase: false) + end + + it 'returns 100 KB for size_limit' do + expect(described_class.size_limit).to eq(100.kilobytes) + end + + it 'returns 10 KB for collapse_limit' do + expect(described_class.collapse_limit).to eq(10.kilobytes) + end + end + end + describe '.new' do context 'using a Hash' do context 'with a small diff' do @@ -47,7 +77,7 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - diff = described_class.new(diff: 'a' * 204800) + diff = described_class.new(diff: 'a' * (described_class.size_limit + 1)) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -60,7 +90,7 @@ EOT let(:diff) { described_class.new(@rugged_diff) } it 'initializes the diff' do - expect(diff.to_hash).to eq(@raw_diff_hash.merge(too_large: nil)) + expect(diff.to_hash).to eq(@raw_diff_hash) end it 'does not prune the diff' do @@ -70,8 +100,8 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - expect_any_instance_of(String).to receive(:bytesize). - and_return(1024 * 1024 * 1024) + expect_any_instance_of(String).to receive(:bytesize) + .and_return(1024 * 1024 * 1024) diff = described_class.new(@rugged_diff) @@ -85,12 +115,12 @@ EOT # The patch total size is 200, with lines between 21 and 54. # This is a quick-and-dirty way to test this. Ideally, a new patch is # added to the test repo with a size that falls between the real limits. - stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150) - stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100) + allow(Gitlab::Git::Diff).to receive(:size_limit).and_return(150) + allow(Gitlab::Git::Diff).to receive(:collapse_limit).and_return(100) end it 'prunes the diff as a large diff instead of as a collapsed diff' do - diff = described_class.new(@rugged_diff, collapse: true) + diff = described_class.new(@rugged_diff, expanded: false) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -100,8 +130,8 @@ EOT context 'using a large binary diff' do it 'does not prune the diff' do - expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?). - and_return(true) + expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?) + .and_return(true) diff = described_class.new(@rugged_diff) @@ -110,23 +140,23 @@ EOT end end - context 'using a Gitaly::CommitDiffResponse' do + context 'using a GitalyClient::Diff' do let(:diff) do described_class.new( - Gitaly::CommitDiffResponse.new( + Gitlab::GitalyClient::Diff.new( to_path: ".gitmodules", from_path: ".gitmodules", old_mode: 0100644, new_mode: 0100644, from_id: '357406f3075a57708d0163752905cc1576fceacc', to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0', - raw_chunks: raw_chunks, + patch: raw_patch ) ) end context 'with a small diff' do - let(:raw_chunks) { [@raw_diff_hash[:diff]] } + let(:raw_patch) { @raw_diff_hash[:diff] } it 'initializes the diff' do expect(diff.to_hash).to eq(@raw_diff_hash) @@ -138,13 +168,21 @@ EOT end context 'using a diff that is too large' do - let(:raw_chunks) { ['a' * 204800] } + let(:raw_patch) { 'a' * 204800 } it 'prunes the diff' do expect(diff.diff).to be_empty expect(diff).to be_too_large end end + + context 'when the patch passed is not UTF-8-encoded' do + let(:raw_patch) { @raw_diff_hash[:diff].encode(Encoding::ASCII_8BIT) } + + it 'encodes diff patch to UTF-8' do + expect(diff.diff).to be_utf8 + end + end end end @@ -203,7 +241,7 @@ EOT end describe '.filter_diff_options' do - let(:options) { { max_size: 100, invalid_opt: true } } + let(:options) { { max_files: 100, invalid_opt: true } } context "without default options" do let(:filtered_options) { described_class.filter_diff_options(options) } @@ -215,7 +253,7 @@ EOT context "with default options" do let(:filtered_options) do - default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true } + default_options = { max_files: 5, bad_opt: 1, ignore_whitespace_change: true } described_class.filter_diff_options(options, default_options) end @@ -225,12 +263,12 @@ EOT end it "should merge with default options" do - expect(filtered_options).to have_key(:ignore_whitespace) + expect(filtered_options).to have_key(:ignore_whitespace_change) end it "should override default options" do - expect(filtered_options).to have_key(:max_size) - expect(filtered_options[:max_size]).to eq(100) + expect(filtered_options).to have_key(:max_files) + expect(filtered_options[:max_files]).to eq(100) end end end @@ -269,7 +307,7 @@ EOT it 'returns true for a diff that was explicitly marked as being too large' do diff = described_class.new(diff: 'a') - diff.prune_large_diff! + diff.too_large! expect(diff.too_large?).to eq(true) end @@ -291,31 +329,31 @@ EOT it 'returns true for a diff that was explicitly marked as being collapsed' do diff = described_class.new(diff: 'a') - diff.prune_collapsed_diff! + diff.collapse! expect(diff).to be_collapsed end end - describe '#collapsible?' do + describe '#collapsed?' do it 'returns true for a diff that is quite large' do - diff = described_class.new(diff: 'a' * 20480) + diff = described_class.new({ diff: 'a' * (described_class.collapse_limit + 1) }, expanded: false) - expect(diff).to be_collapsible + expect(diff).to be_collapsed end it 'returns false for a diff that is small enough' do - diff = described_class.new(diff: 'a') + diff = described_class.new({ diff: 'a' }, expanded: false) - expect(diff).not_to be_collapsible + expect(diff).not_to be_collapsed end end - describe '#prune_collapsed_diff!' do + describe '#collapse!' do it 'prunes the diff' do diff = described_class.new(diff: "foo\nbar") - diff.prune_collapsed_diff! + diff.collapse! expect(diff.diff).to eq('') expect(diff.line_count).to eq(0) diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb deleted file mode 100644 index f6ac7b23d1d..00000000000 --- a/spec/lib/gitlab/git/encoding_helper_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -require "spec_helper" - -describe Gitlab::Git::EncodingHelper do - let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } } - let(:binary_string) { File.join(SEED_STORAGE_PATH, 'gitlab_logo.png') } - - describe '#encode!' do - [ - [ - 'leaves ascii only string as is', - 'ascii only string', - 'ascii only string' - ], - [ - 'leaves valid utf8 string as is', - 'multibyte string №∑∉', - 'multibyte string №∑∉' - ], - [ - 'removes invalid bytes from ASCII-8bit encoded multibyte string. This can occur when a git diff match line truncates in the middle of a multibyte character. This occurs after the second word in this example. The test string is as short as we can get while still triggering the error condition when not looking at `detect[:confidence]`.', - "mu ns\xC3\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ".force_encoding('ASCII-8BIT'), - "mu ns\n Lorem ipsum dolor sit amet, consectetur adipisicing ut\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg kia elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non p\n {: .normal_pn}\n \n-Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in\n# *Lorem ipsum\xC3\xB9l\xC3\xB9l\xC3\xA0 dolor\xC3\xB9k\xC3\xB9 sit\xC3\xA8b\xC3\xA8 N\xC3\xA8 amet b\xC3\xA0d\xC3\xAC*\n+# *consectetur\xC3\xB9l\xC3\xB9l\xC3\xA0 adipisicing\xC3\xB9k\xC3\xB9 elit\xC3\xA8b\xC3\xA8 N\xC3\xA8 sed do\xC3\xA0d\xC3\xAC*{: .italic .smcaps}\n \n \xEF\x9B\xA1 eiusmod tempor incididunt, ut\xC3\xAAn\xC3\xB9 labore et dolore. Tw\xC4\x83nj\xC3\xAC magna aliqua. Ut enim ad minim veniam\n {: .normal}\n@@ -9,5 +9,5 @@ quis nostrud\xC3\xAAt\xC3\xB9 exercitiation ullamco laboris m\xC3\xB9s\xC3\xB9k\xC3\xB9abc\xC3\xB9 nisi ", - ], - ].each do |description, test_string, xpect| - it description do - expect(ext_class.encode!(test_string)).to eq(xpect) - end - end - - it 'leaves binary string as is' do - expect(ext_class.encode!(binary_string)).to eq(binary_string) - end - end - - describe '#encode_utf8' do - [ - [ - "encodes valid utf8 encoded string to utf8", - "λ, λ, λ".encode("UTF-8"), - "λ, λ, λ".encode("UTF-8"), - ], - [ - "encodes valid ASCII-8BIT encoded string to utf8", - "ascii only".encode("ASCII-8BIT"), - "ascii only".encode("UTF-8"), - ], - [ - "encodes valid ISO-8859-1 encoded string to utf8", - "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("ISO-8859-1", "UTF-8"), - "Rüby ist eine Programmiersprache. Wir verlängern den text damit ICU die Sprache erkennen kann.".encode("UTF-8"), - ], - ].each do |description, test_string, xpect| - it description do - r = ext_class.encode_utf8(test_string.force_encoding('UTF-8')) - expect(r).to eq(xpect) - expect(r.encoding.name).to eq('UTF-8') - end - end - - it 'returns empty string on conversion errors' do - expect { ext_class.encode_utf8('') }.not_to raise_error(ArgumentError) - end - end - - describe '#clean' do - [ - [ - 'leaves ascii only string as is', - 'ascii only string', - 'ascii only string' - ], - [ - 'leaves valid utf8 string as is', - 'multibyte string №∑∉', - 'multibyte string №∑∉' - ], - [ - 'removes invalid bytes from ASCII-8bit encoded multibyte string.', - "Lorem ipsum\xC3\n dolor sit amet, xy\xC3\xA0y\xC3\xB9abcd\xC3\xB9efg".force_encoding('ASCII-8BIT'), - "Lorem ipsum\n dolor sit amet, xyà yùabcdùefg", - ], - ].each do |description, test_string, xpect| - it description do - expect(ext_class.encode!(test_string)).to eq(xpect) - end - end - end -end diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb new file mode 100644 index 00000000000..143aa2218c9 --- /dev/null +++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Gitlab::Git::GitmodulesParser do + it 'should parse a .gitmodules file correctly' do + parser = described_class.new(<<-'GITMODULES'.strip_heredoc) + [submodule "vendor/libgit2"] + path = vendor/libgit2 + [submodule "vendor/libgit2"] + url = https://github.com/nodegit/libgit2.git + + # a comment + [submodule "moved"] + path = new/path + url = https://example.com/some/project + [submodule "bogus"] + url = https://example.com/another/project + GITMODULES + + modules = parser.parse + + expect(modules).to eq({ + 'vendor/libgit2' => { 'name' => 'vendor/libgit2', + 'url' => 'https://github.com/nodegit/libgit2.git' }, + 'new/path' => { 'name' => 'moved', + 'url' => 'https://example.com/some/project' } + }) + end +end diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb index 3f279c21865..19391a70cf6 100644 --- a/spec/lib/gitlab/git/hook_spec.rb +++ b/spec/lib/gitlab/git/hook_spec.rb @@ -1,21 +1,29 @@ require 'spec_helper' require 'fileutils' -describe Gitlab::Git::Hook, lib: true do +describe Gitlab::Git::Hook do + before do + # We need this because in the spec/spec_helper.rb we define it like this: + # allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + allow_any_instance_of(described_class).to receive(:trigger).and_call_original + end + describe "#trigger" do let(:project) { create(:project, :repository) } + let(:repo_path) { project.repository.path } let(:user) { create(:user) } + let(:gl_id) { Gitlab::GlId.gl_id(user) } def create_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write('exit 0') end end def create_failing_hook(name) - FileUtils.mkdir_p(File.join(project.repository.path, 'hooks')) - File.open(File.join(project.repository.path, 'hooks', name), 'w', 0755) do |f| + FileUtils.mkdir_p(File.join(repo_path, 'hooks')) + File.open(File.join(repo_path, 'hooks', name), 'w', 0755) do |f| f.write(<<-HOOK) echo 'regular message from the hook' echo 'error message from the hook' 1>&2 @@ -27,13 +35,29 @@ describe Gitlab::Git::Hook, lib: true do ['pre-receive', 'post-receive', 'update'].each do |hook_name| context "when triggering a #{hook_name} hook" do context "when the hook is successful" do + let(:hook_path) { File.join(repo_path, 'hooks', hook_name) } + let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } + let(:env) do + { + 'GL_ID' => gl_id, + 'PWD' => repo_path, + 'GL_PROTOCOL' => 'web', + 'GL_REPOSITORY' => gl_repository + } + end + it "returns success with no errors" do create_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = described_class.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + if hook_name != 'update' + expect(Open3).to receive(:popen3) + .with(env, hook_path, chdir: repo_path).and_call_original + end + + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_blank end @@ -42,11 +66,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook is unsuccessful" do it "returns failure with errors" do create_failing_hook(hook_name) - hook = Gitlab::Git::Hook.new(hook_name, project.repository.path) + hook = described_class.new(hook_name, project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be false expect(errors).to eq("error message from the hook\n") end @@ -56,11 +80,11 @@ describe Gitlab::Git::Hook, lib: true do context "when the hook doesn't exist" do it "returns success with no errors" do - hook = Gitlab::Git::Hook.new('unknown_hook', project.repository.path) + hook = described_class.new('unknown_hook', project) blank = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - status, errors = hook.trigger(Gitlab::GlId.gl_id(user), blank, blank, ref) + status, errors = hook.trigger(gl_id, blank, blank, ref) expect(status).to be true expect(errors).to be_nil end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f88653cb1fe..4ef5d9070a2 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1,14 +1,27 @@ require "spec_helper" describe Gitlab::Git::Repository, seed_helper: true do - include Gitlab::Git::EncodingHelper + include Gitlab::EncodingHelper + + shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method| + it 'wraps gRPC not found error' do + expect_any_instance_of(gitaly_client_class).to receive(gitaly_client_method) + .and_raise(GRPC::NotFound) + expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + + it 'wraps gRPC unknown error' do + expect_any_instance_of(gitaly_client_class).to receive(gitaly_client_method) + .and_raise(GRPC::Unknown) + expect { subject }.to raise_error(Gitlab::Git::CommandError) + end + end let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } describe "Respond to" do subject { repository } - it { is_expected.to respond_to(:raw) } it { is_expected.to respond_to(:rugged) } it { is_expected.to respond_to(:root_ref) } it { is_expected.to respond_to(:tags) } @@ -16,7 +29,9 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#root_ref' do context 'with gitaly disabled' do - before { allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) } + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + end it 'calls #discover_default_branch' do expect(repository).to receive(:discover_default_branch) @@ -24,24 +39,35 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - # TODO: Uncomment when feature is reenabled - # context 'with gitaly enabled' do - # before { stub_gitaly } - # - # it 'gets the branch name from GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name) - # repository.root_ref - # end - # - # it 'wraps GRPC exceptions' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name). - # and_raise(GRPC::Unknown) - # expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError) - # end - # end + it 'returns UTF-8' do + expect(repository.root_ref).to be_utf8 + end + + it 'gets the branch name from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:default_branch_name) + repository.root_ref + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :default_branch_name do + subject { repository.root_ref } + end end describe "#rugged" do + describe 'when storage is broken', broken_storage: true do + it 'raises a storage exception when storage is not available' do + broken_repo = described_class.new('broken', 'a/path.git') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible) + end + end + + it 'raises a no repository exception when there is no repo' do + broken_repo = described_class.new('default', 'a/path.git') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + context 'with no Git env stored' do before do expect(Gitlab::Git::Env).to receive(:all).and_return({}) @@ -110,34 +136,35 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'has SeedRepo::Repo::BRANCHES.size elements' do expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size) end + + it 'returns UTF-8' do + expect(subject.first).to be_utf8 + end + it { is_expected.to include("master") } it { is_expected.not_to include("branch-from-space") } - # TODO: Uncomment when feature is reenabled - # context 'with gitaly enabled' do - # before { stub_gitaly } - # - # it 'gets the branch names from GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names) - # subject - # end - # - # it 'wraps GRPC exceptions' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names). - # and_raise(GRPC::Unknown) - # expect { subject }.to raise_error(Gitlab::Git::CommandError) - # end - # end + it 'gets the branch names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:branch_names) + subject + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branch_names end describe '#tag_names' do subject { repository.tag_names } it { is_expected.to be_kind_of Array } + it 'has SeedRepo::Repo::TAGS.size elements' do expect(subject.size).to eq(SeedRepo::Repo::TAGS.size) end + it 'returns UTF-8' do + expect(subject.first).to be_utf8 + end + describe '#last' do subject { super().last } it { is_expected.to eq("v1.2.1") } @@ -145,21 +172,12 @@ describe Gitlab::Git::Repository, seed_helper: true do it { is_expected.to include("v1.0.0") } it { is_expected.not_to include("v5.0.0") } - # TODO: Uncomment when feature is reenabled - # context 'with gitaly enabled' do - # before { stub_gitaly } - # - # it 'gets the tag names from GitalyClient' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names) - # subject - # end - # - # it 'wraps GRPC exceptions' do - # expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names). - # and_raise(GRPC::Unknown) - # expect { subject }.to raise_error(Gitlab::Git::CommandError) - # end - # end + it 'gets the tag names from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names) + subject + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names end shared_examples 'archive check' do |extenstion| @@ -229,33 +247,6 @@ describe Gitlab::Git::Repository, seed_helper: true do it { expect(repository.bare?).to be_truthy } end - describe '#heads' do - let(:heads) { repository.heads } - subject { heads } - - it { is_expected.to be_kind_of Array } - - describe '#size' do - subject { super().size } - it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) } - end - - context :head do - subject { heads.first } - - describe '#name' do - subject { super().name } - it { is_expected.to eq("feature") } - end - - context :commit do - subject { heads.first.dereferenced_target.sha } - - it { is_expected.to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") } - end - end - end - describe '#ref_names' do let(:ref_names) { repository.ref_names } subject { ref_names } @@ -273,39 +264,41 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#search_files' do - let(:results) { repository.search_files('rails', 'master') } - subject { results } + describe '#submodule_url_for' do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } + let(:ref) { 'master' } - it { is_expected.to be_kind_of Array } + def submodule_url(path) + repository.submodule_url_for(ref, path) + end - describe '#first' do - subject { super().first } - it { is_expected.to be_kind_of Gitlab::Git::BlobSnippet } + it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('nested/six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('deeper/nested/six')).to eq('git://github.com/randx/six.git') } + it { expect(submodule_url('invalid/path')).to eq(nil) } + + context 'uncommitted submodule dir' do + let(:ref) { 'fix-existing-submodule-dir' } + + it { expect(submodule_url('submodule-existing-dir')).to eq(nil) } end - context 'blob result' do - subject { results.first } + context 'tags' do + let(:ref) { 'v1.2.1' } - describe '#ref' do - subject { super().ref } - it { is_expected.to eq('master') } - end + it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') } + end - describe '#filename' do - subject { super().filename } - it { is_expected.to eq('CHANGELOG') } - end + context 'no .gitmodules at commit' do + let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' } - describe '#startline' do - subject { super().startline } - it { is_expected.to eq(35) } - end + it { expect(submodule_url('six')).to eq(nil) } + end - describe '#data' do - subject { super().data } - it { is_expected.to include "Ability to filter by multiple labels" } - end + context 'no gitlink entry' do + let(:ref) { '6d39438' } + + it { expect(submodule_url('six')).to eq(nil) } end end @@ -313,7 +306,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } context 'where repo has submodules' do - let(:submodules) { repository.submodules('master') } + let(:submodules) { repository.send(:submodules, 'master') } let(:submodule) { submodules.first } it { expect(submodules).to be_kind_of Hash } @@ -323,7 +316,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(submodule).to eq([ "six", { "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "path" => "six", + "name" => "six", "url" => "git://github.com/randx/six.git" } ]) @@ -331,14 +324,14 @@ describe Gitlab::Git::Repository, seed_helper: true do it 'should handle nested submodules correctly' do nested = submodules['nested/six'] - expect(nested['path']).to eq('nested/six') + expect(nested['name']).to eq('nested/six') expect(nested['url']).to eq('git://github.com/randx/six.git') expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') end it 'should handle deeply nested submodules correctly' do nested = submodules['deeper/nested/six'] - expect(nested['path']).to eq('deeper/nested/six') + expect(nested['name']).to eq('deeper/nested/six') expect(nested['url']).to eq('git://github.com/randx/six.git') expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196') end @@ -348,25 +341,38 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'should not have an entry for an uncommited submodule dir' do - submodules = repository.submodules('fix-existing-submodule-dir') + submodules = repository.send(:submodules, 'fix-existing-submodule-dir') expect(submodules).not_to have_key('submodule-existing-dir') end it 'should handle tags correctly' do - submodules = repository.submodules('v1.2.1') + submodules = repository.send(:submodules, 'v1.2.1') expect(submodules.first).to eq([ "six", { "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d", - "path" => "six", + "name" => "six", "url" => "git://github.com/randx/six.git" } ]) end + + it 'should not break on invalid syntax' do + allow(repository).to receive(:blob_content).and_return(<<-GITMODULES.strip_heredoc) + [submodule "six"] + path = six + url = git://github.com/randx/six.git + + [submodule] + foo = bar + GITMODULES + + expect(submodules).to have_key('six') + end end context 'where repo doesn\'t have submodules' do - let(:submodules) { repository.submodules('6d39438') } + let(:submodules) { repository.send(:submodules, '6d39438') } it 'should return an empty hash' do expect(submodules).to be_empty end @@ -374,145 +380,20 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#commit_count' do - it { expect(repository.commit_count("master")).to eq(25) } - it { expect(repository.commit_count("feature")).to eq(9) } - end - - describe "#reset" do - change_path = File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, "CHANGELOG") - untracked_path = File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, "UNTRACKED") - tracked_path = File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, "files", "ruby", "popen.rb") - - change_text = "New changelog text" - untracked_text = "This file is untracked" - - reset_commit = SeedRepo::LastCommit::ID - - context "--hard" do - before(:all) do - # Modify a tracked file - File.open(change_path, "w") do |f| - f.write(change_text) - end - - # Add an untracked file to the working directory - File.open(untracked_path, "w") do |f| - f.write(untracked_text) - end - - @normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) - @normal_repo.reset("HEAD", :hard) - end - - it "should replace the working directory with the content of the index" do - File.open(change_path, "r") do |f| - expect(f.each_line.first).not_to eq(change_text) - end - - File.open(tracked_path, "r") do |f| - expect(f.each_line.to_a[8]).to include('raise RuntimeError, "System commands') - end - end - - it "should not touch untracked files" do - expect(File.exist?(untracked_path)).to be_truthy - end - - it "should move the HEAD to the correct commit" do - new_head = @normal_repo.rugged.head.target.oid - expect(new_head).to eq(reset_commit) - end - - it "should move the tip of the master branch to the correct commit" do - new_tip = @normal_repo.rugged.references["refs/heads/master"]. - target.oid - - expect(new_tip).to eq(reset_commit) - end - - after(:all) do - # Fast-forward to the original HEAD - FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) - ensure_seeds - end + shared_examples 'simple commit counting' do + it { expect(repository.commit_count("master")).to eq(25) } + it { expect(repository.commit_count("feature")).to eq(9) } end - end - - describe "#checkout" do - new_branch = "foo_branch" - context "-b" do - before(:all) do - @normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) - @normal_repo.checkout(new_branch, { b: true }, "origin/feature") - end - - it "should create a new branch" do - expect(@normal_repo.rugged.branches[new_branch]).not_to be_nil - end - - it "should move the HEAD to the correct commit" do - expect(@normal_repo.rugged.head.target.oid).to( - eq(@normal_repo.rugged.branches["origin/feature"].target.oid) - ) - end - - it "should refresh the repo's #heads collection" do - head_names = @normal_repo.heads.map { |h| h.name } - expect(head_names).to include(new_branch) - end - - after(:all) do - FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) - ensure_seeds + context 'when Gitaly commit_count feature is enabled' do + it_behaves_like 'simple commit counting' + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do + subject { repository.commit_count('master') } end end - context "without -b" do - context "and specifying a nonexistent branch" do - it "should not do anything" do - normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) - - expect { normal_repo.checkout(new_branch) }.to raise_error(Rugged::ReferenceError) - expect(normal_repo.rugged.branches[new_branch]).to be_nil - expect(normal_repo.rugged.head.target.oid).to( - eq(normal_repo.rugged.branches["master"].target.oid) - ) - - head_names = normal_repo.heads.map { |h| h.name } - expect(head_names).not_to include(new_branch) - end - - after(:all) do - FileUtils.rm_rf(TEST_NORMAL_REPO_PATH) - ensure_seeds - end - end - - context "and with a valid branch" do - before(:all) do - @normal_repo = Gitlab::Git::Repository.new('default', TEST_NORMAL_REPO_PATH) - @normal_repo.rugged.branches.create("feature", "origin/feature") - @normal_repo.checkout("feature") - end - - it "should move the HEAD to the correct commit" do - expect(@normal_repo.rugged.head.target.oid).to( - eq(@normal_repo.rugged.branches["feature"].target.oid) - ) - end - - it "should update the working directory" do - File.open(File.join(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH, ".gitignore"), "r") do |f| - expect(f.read.each_line.to_a).not_to include(".DS_Store\n") - end - end - - after(:all) do - FileUtils.rm_rf(SEED_STORAGE_PATH, TEST_NORMAL_REPO_PATH) - ensure_seeds - end - end + context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'simple commit counting' end end @@ -526,10 +407,6 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(@repo.rugged.branches["feature"]).to be_nil end - it "should update the repo's #heads collection" do - expect(@repo.heads).not_to include("feature") - end - after(:all) do FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) ensure_seeds @@ -551,11 +428,11 @@ describe Gitlab::Git::Repository, seed_helper: true do it "should fail if we create an existing branch" do @repo.create_branch('duplicated_branch', 'master') - expect{@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") + expect {@repo.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") end it "should fail if we create a branch from a non existing ref" do - expect{@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") + expect {@repo.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") end after(:all) do @@ -634,17 +511,22 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#log" do - commit_with_old_name = nil - commit_with_new_name = nil - rename_commit = nil + let(:commit_with_old_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id) + end + let(:commit_with_new_name) do + Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id) + end + let(:rename_commit) do + Gitlab::Git::Commit.decorate(repository, @rename_commit_id) + end before(:context) do # Add new commits so that there's a renamed file in the commit history repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH).rugged - - commit_with_old_name = new_commit_edit_old_file(repo) - rename_commit = new_commit_move_file(repo) - commit_with_new_name = new_commit_edit_new_file(repo) + @commit_with_old_name_id = new_commit_edit_old_file(repo) + @rename_commit_id = new_commit_move_file(repo) + @commit_with_new_name_id = new_commit_edit_new_file(repo) end after(:context) do @@ -817,8 +699,8 @@ describe Gitlab::Git::Repository, seed_helper: true do context "compare results between log_by_walk and log_by_shell" do let(:options) { { ref: "master" } } - let(:commits_by_walk) { repository.log(options).map(&:oid) } - let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:oid) } + let(:commits_by_walk) { repository.log(options).map(&:id) } + let(:commits_by_shell) { repository.log(options.merge({ disable_walk: true })).map(&:id) } it { expect(commits_by_walk).to eq(commits_by_shell) } @@ -861,7 +743,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(commits.size).to be > 0 expect(commits).to satisfy do |commits| - commits.all? { |commit| commit.time >= options[:after] } + commits.all? { |commit| commit.committed_date >= options[:after] } end end end @@ -874,7 +756,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(commits.size).to be > 0 expect(commits).to satisfy do |commits| - commits.all? { |commit| commit.time <= options[:before] } + commits.all? { |commit| commit.committed_date <= options[:before] } end end end @@ -883,7 +765,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } } def commit_files(commit) - commit.diff(commit.parent_ids.first).deltas.flat_map do |delta| + commit.rugged_diff_from_parent.deltas.flat_map do |delta| [delta.old_file[:path], delta.new_file[:path]].uniq.compact end end @@ -899,13 +781,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#commits_between" do + describe "#rugged_commits_between" do context 'two SHAs' do let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' } it 'returns the number of commits between' do - expect(repository.commits_between(first_sha, second_sha).count).to eq(3) + expect(repository.rugged_commits_between(first_sha, second_sha).count).to eq(3) end end @@ -914,11 +796,11 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:branch) { 'master' } it 'returns the number of commits between a sha and a branch' do - expect(repository.commits_between(sha, branch).count).to eq(5) + expect(repository.rugged_commits_between(sha, branch).count).to eq(5) end it 'returns the number of commits between a branch and a sha' do - expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch + expect(repository.rugged_commits_between(branch, sha).count).to eq(0) # sha is before branch end end @@ -927,7 +809,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:second_branch) { 'master' } it 'returns the number of commits between' do - expect(repository.commits_between(first_branch, second_branch).count).to eq(17) + expect(repository.rugged_commits_between(first_branch, second_branch).count).to eq(17) end end end @@ -939,29 +821,39 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#count_commits' do - context 'with after timestamp' do - it 'returns the number of commits after timestamp' do - options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + shared_examples 'extended commit counting' do + context 'with after timestamp' do + it 'returns the number of commits after timestamp' do + options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } - expect(repository.count_commits(options)).to eq(25) + expect(repository.count_commits(options)).to eq(25) + end end - end - context 'with before timestamp' do - it 'returns the number of commits after timestamp' do - options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } + context 'with before timestamp' do + it 'returns the number of commits before timestamp' do + options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } - expect(repository.count_commits(options)).to eq(9) + expect(repository.count_commits(options)).to eq(9) + end end - end - context 'with path' do - it 'returns the number of commits with path ' do - options = { ref: 'master', limit: nil, path: "encoding" } + context 'with path' do + it 'returns the number of commits with path ' do + options = { ref: 'master', limit: nil, path: "encoding" } - expect(repository.count_commits(options)).to eq(2) + expect(repository.count_commits(options)).to eq(2) + end end end + + context 'when Gitaly count_commits feature is enabled' do + it_behaves_like 'extended commit counting' + end + + context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'extended commit counting' + end end describe "branch_names_contains" do @@ -1031,63 +923,76 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe '#find_commits' do - it 'should return a return a collection of commits' do - commits = repository.find_commits + describe '#ref_name_for_sha' do + let(:ref_path) { 'refs/heads' } + let(:sha) { repository.find_branch('master').dereferenced_target.id } + let(:ref_name) { 'refs/heads/master' } - expect(commits).not_to be_empty - expect(commits).to all( be_a_kind_of(Gitlab::Git::Commit) ) + it 'returns the ref name for the given sha' do + expect(repository.ref_name_for_sha(ref_path, sha)).to eq(ref_name) end - context 'while applying a sort order based on the `order` option' do - it "allows ordering topologically (no parents shown before their children)" do - expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_TOPO) + it "returns an empty name if the ref doesn't exist" do + expect(repository.ref_name_for_sha(ref_path, "000000")).to eq("") + end - repository.find_commits(order: :topo) - end + it "raise an exception if the ref is empty" do + expect { repository.ref_name_for_sha(ref_path, "") }.to raise_error(ArgumentError) + end - it "allows ordering by date" do - expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_DATE) + it "raise an exception if the ref is nil" do + expect { repository.ref_name_for_sha(ref_path, nil) }.to raise_error(ArgumentError) + end + end - repository.find_commits(order: :date) - end + describe '#branches' do + subject { repository.branches } - it "applies no sorting by default" do - expect_any_instance_of(Rugged::Walker).to receive(:sorting).with(Rugged::SORT_NONE) + context 'with local and remote branches' do + let(:repository) do + Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git')) + end - repository.find_commits + before do + create_remote_branch(repository, 'joe', 'remote_branch', 'master') + repository.create_branch('local_branch', 'master') end - end - end - describe '#branches with deleted branch' do - before(:each) do - ref = double() - allow(ref).to receive(:name) { 'bad-branch' } - allow(ref).to receive(:target) { raise Rugged::ReferenceError } - allow(repository.rugged).to receive(:branches) { [ref] } - end + after do + FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH) + ensure_seeds + end - it 'should return empty branches' do - expect(repository.branches).to eq([]) + it 'returns the local and remote branches' do + expect(subject.any? { |b| b.name == 'joe/remote_branch' }).to eq(true) + expect(subject.any? { |b| b.name == 'local_branch' }).to eq(true) + end end - end - describe '#branch_count' do - before(:each) do - valid_ref = double(:ref) - invalid_ref = double(:ref) - - allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) + # With Gitaly enabled, Gitaly just doesn't return deleted branches. + context 'with deleted branch with Gitaly disabled' do + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + end - allow(invalid_ref).to receive_messages(name: 'bad-branch') - allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } + it 'returns no results' do + ref = double() + allow(ref).to receive(:name) { 'bad-branch' } + allow(ref).to receive(:target) { raise Rugged::ReferenceError } + branches = double() + allow(branches).to receive(:each) { [ref].each } + allow(repository.rugged).to receive(:branches) { branches } - allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) + expect(subject).to be_empty + end end + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branches + end + + describe '#branch_count' do it 'returns the number of branches' do - expect(repository.branch_count).to eq(1) + expect(repository.branch_count).to eq(10) end end @@ -1103,7 +1008,7 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(master_file_paths).to include("files/html/500.html") end - it "dose not read submodule directory and empty directory of master branch" do + it "does not read submodule directory and empty directory of master branch" do expect(master_file_paths).not_to include("six") end @@ -1124,7 +1029,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end context "with no .gitattrbutes" do - before(:each) do + before do repository.copy_gitattributes("master") end @@ -1132,13 +1037,13 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(File.exist?(attributes_path)).to be_falsey end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end context "with .gitattrbutes" do - before(:each) do + before do repository.copy_gitattributes("gitattributes") end @@ -1151,13 +1056,13 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(contents).to eq("*.md binary\n") end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end context "with updated .gitattrbutes" do - before(:each) do + before do repository.copy_gitattributes("gitattributes") repository.copy_gitattributes("gitattributes-updated") end @@ -1171,13 +1076,13 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(contents).to eq("*.txt binary\n") end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end context "with no .gitattrbutes in HEAD but with previous info/attributes" do - before(:each) do + before do repository.copy_gitattributes("gitattributes") repository.copy_gitattributes("master") end @@ -1186,53 +1091,12 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(File.exist?(attributes_path)).to be_falsey end - after(:each) do + after do FileUtils.rm_rf(attributes_path) end end end - describe '#diffable' do - info_dir_path = attributes_path = File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info') - attributes_path = File.join(info_dir_path, 'attributes') - - before(:all) do - FileUtils.mkdir(info_dir_path) unless File.exist?(info_dir_path) - File.write(attributes_path, "*.md -diff\n") - end - - it "should return true for files which are text and do not have attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'LICENSE' - ) - expect(repository.diffable?(blob)).to be_truthy - end - - it "should return false for binary files which do not have attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'files/images/logo-white.png' - ) - expect(repository.diffable?(blob)).to be_falsey - end - - it "should return false for text files which have been marked as not being diffable in attributes" do - blob = Gitlab::Git::Blob.find( - repository, - '33bcff41c232a11727ac6d660bd4b0c2ba86d63d', - 'README.md' - ) - expect(repository.diffable?(blob)).to be_falsey - end - - after(:all) do - FileUtils.rm_rf(info_dir_path) - end - end - describe '#tag_exists?' do it 'returns true for an existing tag' do tag = repository.tag_names.first @@ -1261,7 +1125,7 @@ describe Gitlab::Git::Repository, seed_helper: true do describe '#local_branches' do before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH) + @repo = Gitlab::Git::Repository.new('default', File.join(TEST_MUTABLE_REPO_PATH, '.git')) end after(:all) do @@ -1270,17 +1134,75 @@ describe Gitlab::Git::Repository, seed_helper: true do end it 'returns the local branches' do - create_remote_branch('joe', 'remote_branch', 'master') + create_remote_branch(@repo, 'joe', 'remote_branch', 'master') @repo.create_branch('local_branch', 'master') expect(@repo.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) expect(@repo.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) end + + it 'returns a Branch with UTF-8 fields' do + branches = @repo.local_branches.to_a + expect(branches.size).to be > 0 + branches.each do |branch| + expect(branch.name).to be_utf8 + expect(branch.target).to be_utf8 unless branch.target.nil? + end + end + + it 'gets the branches from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:local_branches) + .and_return([]) + @repo.local_branches + end + + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :local_branches do + subject { @repo.local_branches } + end + end + + describe '#languages' do + shared_examples 'languages' do + it 'returns exactly the expected results' do + languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6') + expected_languages = [ + { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" }, + { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" }, + { value: 7.9, label: "HTML", color: "#e44b23", highlight: "#e44b23" }, + { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" } + ] + + expect(languages.size).to eq(expected_languages.size) + + expected_languages.size.times do |i| + a = expected_languages[i] + b = languages[i] + + expect(a.keys.sort).to eq(b.keys.sort) + expect(a[:value]).to be_within(0.1).of(b[:value]) + + non_float_keys = a.keys - [:value] + expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys)) + end + end + + it "uses the repository's HEAD when no ref is passed" do + lang = repository.languages.first + + expect(lang[:label]).to eq('Ruby') + end + end + + it_behaves_like 'languages' + + context 'with rugged', skip_gitaly_mock: true do + it_behaves_like 'languages' + end end - def create_remote_branch(remote_name, branch_name, source_branch_name) - source_branch = @repo.branches.find { |branch| branch.name == source_branch_name } - rugged = @repo.rugged + def create_remote_branch(repository, remote_name, branch_name, source_branch_name) + source_branch = repository.branches.find { |branch| branch.name == source_branch_name } + rugged = repository.rugged rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha) end @@ -1353,11 +1275,4 @@ describe Gitlab::Git::Repository, seed_helper: true do sha = Rugged::Commit.create(repo, options) repo.lookup(sha) end - - def stub_gitaly - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) - - stub = double(:stub) - allow(Gitaly::Ref::Stub).to receive(:new).and_return(stub) - end end diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb index 78894ba9409..b051a088171 100644 --- a/spec/lib/gitlab/git/rev_list_spec.rb +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Gitlab::Git::RevList, lib: true do +describe Gitlab::Git::RevList do let(:project) { create(:project, :repository) } before do @@ -11,7 +11,7 @@ describe Gitlab::Git::RevList, lib: true do end context "#new_refs" do - let(:rev_list) { Gitlab::Git::RevList.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + let(:rev_list) { described_class.new(newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } it 'calls out to `popen`' do expect(Gitlab::Popen).to receive(:popen).with([ @@ -33,7 +33,7 @@ describe Gitlab::Git::RevList, lib: true do end context "#missed_ref" do - let(:rev_list) { Gitlab::Git::RevList.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } + let(:rev_list) { described_class.new(oldrev: 'oldrev', newrev: 'newrev', path_to_repo: project.repository.path_to_repo) } it 'calls out to `popen`' do expect(Gitlab::Popen).to receive(:popen).with([ diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb new file mode 100644 index 00000000000..c86353abb7c --- /dev/null +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -0,0 +1,329 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do + let(:storage_name) { 'default' } + let(:circuit_breaker) { described_class.new(storage_name) } + let(:hostname) { Gitlab::Environment.hostname } + let(:cache_key) { "storage_accessible:#{storage_name}:#{hostname}" } + + before do + # Override test-settings for the circuitbreaker with something more realistic + # for these specs. + stub_storage_settings('default' => { + 'path' => TestEnv.repos_path, + 'failure_count_threshold' => 10, + 'failure_wait_time' => 30, + 'failure_reset_time' => 1800, + 'storage_timeout' => 5 + }, + 'broken' => { + 'path' => 'tmp/tests/non-existent-repositories', + 'failure_count_threshold' => 10, + 'failure_wait_time' => 30, + 'failure_reset_time' => 1800, + 'storage_timeout' => 5 + } + ) + end + + def value_from_redis(name) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, name) + end.first + end + + def set_in_redis(name, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, name, value) + end.first + end + + describe '.reset_all!' do + it 'clears all entries form redis' do + set_in_redis(:failure_count, 10) + + described_class.reset_all! + + key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) } + + expect(key_exists).to be_falsey + end + end + + describe '.for_storage' do + it 'only builds a single circuitbreaker per storage' do + expect(described_class).to receive(:new).once.and_call_original + + breaker = described_class.for_storage('default') + + expect(breaker).to be_a(described_class) + expect(described_class.for_storage('default')).to eq(breaker) + end + end + + describe '#initialize' do + it 'assigns the settings' do + expect(circuit_breaker.hostname).to eq(hostname) + expect(circuit_breaker.storage).to eq('default') + expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path) + expect(circuit_breaker.failure_count_threshold).to eq(10) + expect(circuit_breaker.failure_wait_time).to eq(30) + expect(circuit_breaker.failure_reset_time).to eq(1800) + expect(circuit_breaker.storage_timeout).to eq(5) + end + end + + describe '#perform' do + it 'raises an exception with retry time when the circuit is open' do + allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + + expect { |b| circuit_breaker.perform(&b) } + .to raise_error(Gitlab::Git::Storage::CircuitOpen) + end + + it 'yields the block' do + expect { |b| circuit_breaker.perform(&b) } + .to yield_control + end + + it 'checks if the storage is available' do + expect(circuit_breaker).to receive(:check_storage_accessible!) + + circuit_breaker.perform { 'hello world' } + end + + it 'returns the value of the block' do + result = circuit_breaker.perform { 'return value' } + + expect(result).to eq('return value') + end + + it 'raises possible errors' do + expect { circuit_breaker.perform { raise Rugged::OSError.new('Broken') } } + .to raise_error(Rugged::OSError) + end + + context 'with the feature disabled' do + it 'returns the block without checking accessibility' do + stub_feature_flags(git_storage_circuit_breaker: false) + + expect(circuit_breaker).not_to receive(:circuit_broken?) + + result = circuit_breaker.perform { 'hello' } + + expect(result).to eq('hello') + end + end + end + + describe '#circuit_broken?' do + it 'is working when there is no last failure' do + set_in_redis(:last_failure, nil) + set_in_redis(:failure_count, 0) + + expect(circuit_breaker.circuit_broken?).to be_falsey + end + + it 'is broken when there was a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.circuit_broken?).to be_truthy + end + end + + it 'is broken when there are too many failures' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 200) + + expect(circuit_breaker.circuit_broken?).to be_truthy + end + + context 'the `failure_wait_time` is set to 0' do + before do + stub_storage_settings('default' => { + 'failure_wait_time' => 0, + 'path' => TestEnv.repos_path + }) + end + + it 'is working even when there is a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 0.seconds.ago.to_f) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.circuit_broken?).to be_falsey + end + end + end + end + + describe "storage_available?" do + context 'the storage is available' do + it 'tracks that the storage was accessible an raises the error' do + expect(circuit_breaker).to receive(:track_storage_accessible) + + circuit_breaker.storage_available? + end + + it 'only performs the check once' do + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).once.and_call_original + + 2.times { circuit_breaker.storage_available? } + end + end + + context 'storage is not available' do + let(:storage_name) { 'broken' } + + it 'tracks that the storage was inaccessible' do + expect(circuit_breaker).to receive(:track_storage_inaccessible) + + circuit_breaker.storage_available? + end + end + end + + describe '#check_storage_accessible!' do + it 'raises an exception with retry time when the circuit is open' do + allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + + expect { circuit_breaker.check_storage_accessible! } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) + expect(exception.retry_after).to eq(30) + end + end + + context 'the storage is not available' do + let(:storage_name) { 'broken' } + + it 'raises an error' do + expect(circuit_breaker).to receive(:track_storage_inaccessible) + + expect { circuit_breaker.check_storage_accessible! } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible) + expect(exception.retry_after).to eq(30) + end + end + end + end + + describe '#track_storage_inaccessible' do + around do |example| + Timecop.freeze { example.run } + end + + it 'records the failure time in redis' do + circuit_breaker.track_storage_inaccessible + + failure_time = value_from_redis(:last_failure) + + expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now) + end + + it 'sets the failure time on the breaker without reloading' do + circuit_breaker.track_storage_inaccessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.last_failure).to eq(Time.now) + end + + it 'increments the failure count in redis' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_inaccessible + + expect(value_from_redis(:failure_count).to_i).to be(11) + end + + it 'increments the failure count on the breaker without reloading' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_inaccessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.failure_count).to eq(11) + end + end + + describe '#track_storage_accessible' do + it 'sets the failure count to zero in redis' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_accessible + + expect(value_from_redis(:failure_count).to_i).to be(0) + end + + it 'sets the failure count to zero on the breaker without reloading' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_accessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.failure_count).to eq(0) + end + + it 'removes the last failure time from redis' do + set_in_redis(:last_failure, Time.now.to_i) + + circuit_breaker.track_storage_accessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.last_failure).to be_nil + end + + it 'removes the last failure time from the breaker without reloading' do + set_in_redis(:last_failure, Time.now.to_i) + + circuit_breaker.track_storage_accessible + + expect(value_from_redis(:last_failure)).to be_empty + end + + it 'wont connect to redis when there are no failures' do + expect(Gitlab::Git::Storage.redis).to receive(:with).once + .and_call_original + expect(circuit_breaker).to receive(:track_storage_accessible) + .and_call_original + + circuit_breaker.track_storage_accessible + end + end + + describe '#no_failures?' do + it 'is false when a failure was tracked' do + set_in_redis(:last_failure, Time.now.to_i) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.no_failures?).to be_falsey + end + end + + describe '#last_failure' do + it 'returns the last failure time' do + time = Time.parse("2017-05-26 17:52:30") + set_in_redis(:last_failure, time.to_i) + + expect(circuit_breaker.last_failure).to eq(time) + end + end + + describe '#failure_count' do + it 'returns the failure count' do + set_in_redis(:failure_count, 7) + + expect(circuit_breaker.failure_count).to eq(7) + end + end + + describe '#cache_key' do + it 'includes storage and host' do + expect(circuit_breaker.cache_key).to eq(cache_key) + end + end +end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb new file mode 100644 index 00000000000..12366151f44 --- /dev/null +++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true do + let(:existing_path) do + existing_path = TestEnv.repos_path + FileUtils.mkdir_p(existing_path) + existing_path + end + + describe '.storage_accessible?' do + it 'detects when a storage is not available' do + expect(described_class.storage_available?('/non/existant/path')).to be_falsey + end + + it 'detects when a storage is available' do + expect(described_class.storage_available?(existing_path)).to be_truthy + end + + it 'returns false when the check takes to long' do + # We're forking a process here that takes too long + # It will be killed it's parent process will be killed by it's parent + # and waited for inside `Gitlab::Git::Storage::ForkedStorageCheck.timeout_check` + allow(described_class).to receive(:check_filesystem_in_process) do + Process.spawn("sleep 10") + end + result = true + + runtime = Benchmark.realtime do + result = described_class.storage_available?(existing_path, 0.5) + end + + expect(result).to be_falsey + expect(runtime).to be < 1.0 + end + + describe 'when using paths with spaces' do + let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') } + let(:path_with_spaces) { File.join(test_dir, 'path with spaces') } + + around do |example| + FileUtils.mkdir_p(path_with_spaces) + example.run + FileUtils.rm_r(test_dir) + end + + it 'works for paths with spaces' do + expect(described_class.storage_available?(path_with_spaces)).to be_truthy + end + + it 'works for a realpath with spaces' do + symlink_location = File.join(test_dir, 'a symlink') + FileUtils.ln_s(path_with_spaces, symlink_location) + + expect(described_class.storage_available?(symlink_location)).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb new file mode 100644 index 00000000000..2d3af387971 --- /dev/null +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do + let(:host1_key) { 'storage_accessible:broken:web01' } + let(:host2_key) { 'storage_accessible:default:kiq01' } + + def set_in_redis(cache_key, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, :failure_count, value) + end.first + end + + describe '.for_failing_storages' do + it 'only includes health status for failures' do + set_in_redis(host1_key, 10) + set_in_redis(host2_key, 0) + + expect(described_class.for_failing_storages.map(&:storage_name)) + .to contain_exactly('broken') + end + end + + describe '.load_for_keys' do + let(:subject) do + results = Gitlab::Git::Storage.redis.with do |redis| + fake_future = double + allow(fake_future).to receive(:value).and_return([host1_key]) + described_class.load_for_keys({ 'broken' => fake_future }, redis) + end + + # Make sure the `Redis#future is loaded + results.inject({}) do |result, (name, info)| + info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } + + result[name] = info + + result + end + end + + it 'loads when there is no info in redis' do + expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }]) + end + + it 'reads the correct values for a storage from redis' do + set_in_redis(host1_key, 5) + set_in_redis(host2_key, 7) + + expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }]) + end + end + + describe '.for_all_storages' do + it 'loads health status for all configured storages' do + healths = described_class.for_all_storages + + expect(healths.map(&:storage_name)).to contain_exactly('default', 'broken') + end + end + + describe '#failing_info' do + it 'only contains storages that have failures' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 0 }, + { name: host2_key, failure_count: 3 }]) + + expect(health.failing_info).to contain_exactly({ name: host2_key, failure_count: 3 }) + end + end + + describe '#total_failures' do + it 'sums up all the failures' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, + { name: host2_key, failure_count: 3 }]) + + expect(health.total_failures).to eq(5) + end + end + + describe '#failing_on_hosts' do + it 'collects only the failing hostnames' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, + { name: host2_key, failure_count: 0 }]) + + expect(health.failing_on_hosts).to contain_exactly('web01') + end + end +end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index 67a9c974298..78d1e120013 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -3,23 +3,33 @@ require "spec_helper" describe Gitlab::Git::Tag, seed_helper: true do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } - describe 'first tag' do - let(:tag) { repository.tags.first } + shared_examples 'Gitlab::Git::Repository#tags' do + describe 'first tag' do + let(:tag) { repository.tags.first } - it { expect(tag.name).to eq("v1.0.0") } - it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") } - it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") } - it { expect(tag.message).to eq("Release") } - end + it { expect(tag.name).to eq("v1.0.0") } + it { expect(tag.target).to eq("f4e6814c3e4e7a0de82a9e7cd20c626cc963a2f8") } + it { expect(tag.dereferenced_target.sha).to eq("6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9") } + it { expect(tag.message).to eq("Release") } + end + + describe 'last tag' do + let(:tag) { repository.tags.last } - describe 'last tag' do - let(:tag) { repository.tags.last } + it { expect(tag.name).to eq("v1.2.1") } + it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") } + it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") } + it { expect(tag.message).to eq("Version 1.2.1") } + end - it { expect(tag.name).to eq("v1.2.1") } - it { expect(tag.target).to eq("2ac1f24e253e08135507d0830508febaaccf02ee") } - it { expect(tag.dereferenced_target.sha).to eq("fa1b1e6c004a68b7d8763b86455da9e6b23e36d6") } - it { expect(tag.message).to eq("Version 1.2.1") } + it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) } end - it { expect(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) } + context 'when Gitaly tags feature is enabled' do + it_behaves_like 'Gitlab::Git::Repository#tags' + end + + context 'when Gitaly tags feature is disabled', skip_gitaly_mock: true do + it_behaves_like 'Gitlab::Git::Repository#tags' + end end diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb index 4b76a43e6b5..98ddd3c3664 100644 --- a/spec/lib/gitlab/git/tree_spec.rb +++ b/spec/lib/gitlab/git/tree_spec.rb @@ -1,8 +1,9 @@ require "spec_helper" describe Gitlab::Git::Tree, seed_helper: true do + let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } + context :repo do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) } let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) } it { expect(tree).to be_kind_of Array } @@ -74,4 +75,24 @@ describe Gitlab::Git::Tree, seed_helper: true do it { expect(submodule.name).to eq('gitlab-shell') } end end + + describe '#where' do + context 'with gitaly disabled' do + before do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false) + end + + it 'calls #tree_entries_from_rugged' do + expect(described_class).to receive(:tree_entries_from_rugged) + + described_class.where(repository, SeedRepo::Commit::ID, '/') + end + end + + it 'gets the tree entries from GitalyClient' do + expect_any_instance_of(Gitlab::GitalyClient::CommitService).to receive(:tree_entries) + + described_class.where(repository, SeedRepo::Commit::ID, '/') + end + end end diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb index bcca4d4c746..88c871855df 100644 --- a/spec/lib/gitlab/git/util_spec.rb +++ b/spec/lib/gitlab/git/util_spec.rb @@ -6,10 +6,10 @@ describe Gitlab::Git::Util do ["", 0], ["foo", 1], ["foo\n", 1], - ["foo\n\n", 2], + ["foo\n\n", 2] ].each do |string, line_count| it "counts #{line_count} lines in #{string.inspect}" do - expect(Gitlab::Git::Util.count_lines(string)).to eq(line_count) + expect(described_class.count_lines(string)).to eq(line_count) end end end |