summaryrefslogtreecommitdiff
path: root/spec/lib
diff options
context:
space:
mode:
authorRobert Speicher <rspeicher@gmail.com>2017-01-04 13:43:06 -0500
committerRobert Speicher <rspeicher@gmail.com>2017-01-04 19:09:28 -0500
commita00578ce5cdf4bab95990bca9e806c6322bb1384 (patch)
tree5aafe058fa55086038dbb3086121ad624447c0e4 /spec/lib
parentaec04a47c16665f1dfb1fb61647c3f78a4dde20f (diff)
downloadgitlab-ce-a00578ce5cdf4bab95990bca9e806c6322bb1384.tar.gz
Absorb gitlab_git
Diffstat (limited to 'spec/lib')
-rw-r--r--spec/lib/gitlab/git/attributes_spec.rb150
-rw-r--r--spec/lib/gitlab/git/blame_spec.rb66
-rw-r--r--spec/lib/gitlab/git/blob_snippet_spec.rb19
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb489
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb31
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb408
-rw-r--r--spec/lib/gitlab/git/compare_spec.rb109
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb460
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb287
-rw-r--r--spec/lib/gitlab/git/encoding_helper_spec.rb84
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb1184
-rw-r--r--spec/lib/gitlab/git/tag_spec.rb25
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb76
-rw-r--r--spec/lib/gitlab/git/util_spec.rb16
14 files changed, 3404 insertions, 0 deletions
diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_spec.rb
new file mode 100644
index 00000000000..9c011e34c11
--- /dev/null
+++ b/spec/lib/gitlab/git/attributes_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Attributes, seed_helper: true do
+ let(:path) do
+ File.join(SEED_REPOSITORY_PATH, 'with-git-attributes.git')
+ end
+
+ subject { described_class.new(path) }
+
+ describe '#attributes' do
+ context 'using a path with attributes' do
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.txt')).to eq({ 'text' => true })
+ end
+
+ it 'returns a Hash containing multiple attributes' do
+ 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' })
+ end
+
+ it 'returns a Hash containing attributes for a file in a directory' do
+ expect(subject.attributes('foo/bar.txt')).to eq({ 'foo' => true })
+ 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' })
+ end
+
+ it 'returns a Hash containing the attributes for an absolute path' do
+ expect(subject.attributes('/test.txt')).to eq({ 'text' => true })
+ end
+
+ 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' })
+ end
+
+ it 'returns an empty Hash for a defined path without attributes' do
+ expect(subject.attributes('bla/bla.txt')).to eq({})
+ end
+
+ context 'when the "binary" option is set for a path' do
+ it 'returns true for the "binary" option' do
+ expect(subject.attributes('test.binary')['binary']).to eq(true)
+ end
+
+ it 'returns false for the "diff" option' do
+ expect(subject.attributes('test.binary')['diff']).to eq(false)
+ end
+ end
+ end
+
+ context 'using a path without any attributes' do
+ it 'returns an empty Hash' do
+ expect(subject.attributes('test.foo')).to eq({})
+ end
+ end
+ end
+
+ describe '#patterns' do
+ it 'parses a file with entries' do
+ expect(subject.patterns).to be_an_instance_of(Hash)
+ 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' })
+ end
+
+ it 'stores patterns in reverse order' do
+ first = subject.patterns.to_a[0]
+
+ expect(first[0]).to eq(File.join(path, 'bla/bla.txt'))
+ end
+
+ # It's a bit hard to test for something _not_ being processed. As such we'll
+ # just test the number of entries.
+ it 'ignores any comments and empty lines' do
+ expect(subject.patterns.length).to eq(10)
+ 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(subject.patterns).to eq({})
+ end
+ end
+
+ describe '#parse_attributes' do
+ it 'parses a boolean attribute' do
+ expect(subject.parse_attributes('text')).to eq({ 'text' => true })
+ end
+
+ it 'parses a negated boolean attribute' do
+ expect(subject.parse_attributes('-text')).to eq({ 'text' => false })
+ end
+
+ it 'parses a key-value pair' do
+ expect(subject.parse_attributes('foo=bar')).to eq({ 'foo' => 'bar' })
+ end
+
+ it 'parses multiple attributes' do
+ input = 'boolean key=value -negated'
+
+ 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' })
+ end
+ end
+
+ describe '#each_line' do
+ it 'iterates over every line in the attributes file' do
+ args = [String] * 14 # the number of lines in the file
+
+ expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
+ 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 { |b| subject.each_line(&b) }.not_to yield_control
+ end
+
+ it 'does not yield when the attributes file has an unsupported encoding' do
+ path = File.join(SEED_REPOSITORY_PATH, 'with-invalid-git-attributes.git')
+ attrs = described_class.new(path)
+
+ expect { |b| attrs.each_line(&b) }.not_to yield_control
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb
new file mode 100644
index 00000000000..e169f5af6b6
--- /dev/null
+++ b/spec/lib/gitlab/git/blame_spec.rb
@@ -0,0 +1,66 @@
+# coding: utf-8
+require "spec_helper"
+
+describe Gitlab::Git::Blame, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:blame) 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
+
+ context "ISO-8859 encoding" do
+ let(:blame) do
+ Gitlab::Git::Blame.new(repository, SeedRepo::EncodingCommit::ID, "encoding/iso8859.txt")
+ end
+
+ it 'converts to UTF-8' do
+ data = []
+ blame.each do |commit, line|
+ data << {
+ commit: commit,
+ line: line
+ }
+ 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
+
+ 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
+
+ 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
+end
diff --git a/spec/lib/gitlab/git/blob_snippet_spec.rb b/spec/lib/gitlab/git/blob_snippet_spec.rb
new file mode 100644
index 00000000000..79b1311ac91
--- /dev/null
+++ b/spec/lib/gitlab/git/blob_snippet_spec.rb
@@ -0,0 +1,19 @@
+# encoding: UTF-8
+
+require "spec_helper"
+
+describe Gitlab::Git::BlobSnippet, seed_helper: true do
+ describe :data do
+ context 'empty lines' do
+ let(:snippet) { Gitlab::Git::BlobSnippet.new('master', nil, nil, nil) }
+
+ it { expect(snippet.data).to be_nil }
+ end
+
+ context 'present lines' do
+ let(:snippet) { Gitlab::Git::BlobSnippet.new('master', ['wow', 'much'], 1, 'wow.rb') }
+
+ it { expect(snippet.data).to eq("wow\nmuch") }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
new file mode 100644
index 00000000000..84f79ec2391
--- /dev/null
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -0,0 +1,489 @@
+# encoding: utf-8
+
+require "spec_helper"
+
+describe Gitlab::Git::Blob, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ describe :initialize do
+ let(:blob) { Gitlab::Git::Blob.new(name: 'test') }
+
+ it 'handles nil data' do
+ expect(blob.name).to eq('test')
+ expect(blob.size).to eq(nil)
+ expect(blob.loaded_size).to eq(nil)
+ end
+ end
+
+ describe :find do
+ context 'file in subdir' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb") }
+
+ 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 'file in root' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, ".gitignore") }
+
+ it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
+ it { expect(blob.name).to eq(".gitignore") }
+ it { expect(blob.path).to eq(".gitignore") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
+ it { expect(blob.size).to eq(241) }
+ it { expect(blob.mode).to eq("100644") }
+ it { expect(blob).not_to be_binary }
+ end
+
+ context 'file in root with leading slash' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "/.gitignore") }
+
+ it { expect(blob.id).to eq("dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82") }
+ it { expect(blob.name).to eq(".gitignore") }
+ it { expect(blob.path).to eq(".gitignore") }
+ it { expect(blob.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(blob.data[0..10]).to eq("*.rbc\n*.sas") }
+ it { expect(blob.size).to eq(241) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'non-exist file' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "missing.rb") }
+
+ it { expect(blob).to be_nil }
+ end
+
+ context 'six submodule' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'six') }
+
+ it { expect(blob.id).to eq('409f37c4f05865e4fb208c771485f211a22c4c2d') }
+ it { expect(blob.data).to eq('') }
+
+ it 'does not get messed up by load_all_data!' do
+ blob.load_all_data!(repository)
+ expect(blob.data).to eq('')
+ end
+
+ it 'does not mark the blob as binary' do
+ expect(blob).not_to be_binary
+ end
+ end
+
+ context 'large file' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg') }
+ let(:blob_size) { 111803 }
+
+ it { expect(blob.size).to eq(blob_size) }
+ it { expect(blob.data.length).to eq(blob_size) }
+
+ it 'check that this test is sane' do
+ expect(blob.size).to be <= Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE
+ end
+
+ it 'can load all data' do
+ blob.load_all_data!(repository)
+ expect(blob.data.length).to eq(blob_size)
+ end
+
+ it 'marks the blob as binary' do
+ 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
+ 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") }
+ it { expect(raw_blob.size).to eq(669) }
+ it { expect(raw_blob.truncated?).to be_falsey }
+
+ context 'large file' do
+ it 'limits the size of a large file' do
+ blob_size = Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE + 1
+ buffer = Array.new(blob_size, 0)
+ rugged_blob = Rugged::Blob.from_buffer(repository.rugged, buffer.join(''))
+ blob = Gitlab::Git::Blob.raw(repository, rugged_blob)
+
+ expect(blob.size).to eq(blob_size)
+ expect(blob.loaded_size).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ expect(blob.data.length).to eq(Gitlab::Git::Blob::MAX_DATA_DISPLAY_SIZE)
+ expect(blob.truncated?).to be_truthy
+
+ blob.load_all_data!(repository)
+ expect(blob.loaded_size).to eq(blob_size)
+ 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") }
+
+ it { expect(blob.name).to eq("russian.rb") }
+ it { expect(blob.data.lines.first).to eq("Хороший файл") }
+ it { expect(blob.size).to eq(23) }
+ it { expect(blob.truncated?).to be_falsey }
+ # Run it twice since data is encoded after the first run
+ it { expect(blob.truncated?).to be_falsey }
+ it { expect(blob.mode).to eq("100755") }
+ end
+
+ context 'file with Chinese text' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
+
+ it { expect(blob.name).to eq("テスト.txt") }
+ it { expect(blob.data).to include("これはテスト") }
+ it { expect(blob.size).to eq(340) }
+ it { expect(blob.mode).to eq("100755") }
+ it { expect(blob.truncated?).to be_falsey }
+ end
+
+ context 'file with ISO-8859 text' do
+ let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::LastCommit::ID, "encoding/iso8859.txt") }
+
+ it { expect(blob.name).to eq("iso8859.txt") }
+ it { expect(blob.loaded_size).to eq(4) }
+ it { expect(blob.size).to eq(4) }
+ it { expect(blob.mode).to eq("100644") }
+ it { expect(blob.truncated?).to be_falsey }
+ end
+ end
+
+ describe 'mode' do
+ context 'file regular' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/ruby/regex.rb'
+ )
+ end
+
+ it { expect(blob.name).to eq('regex.rb') }
+ it { expect(blob.path).to eq('files/ruby/regex.rb') }
+ it { expect(blob.size).to eq(1200) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'file binary' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/executables/ls'
+ )
+ end
+
+ it { expect(blob.name).to eq('ls') }
+ it { expect(blob.path).to eq('files/executables/ls') }
+ it { expect(blob.size).to eq(110080) }
+ it { expect(blob.mode).to eq("100755") }
+ end
+
+ context 'file symlink to regular' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/links/ruby-style-guide.md'
+ )
+ end
+
+ it { expect(blob.name).to eq('ruby-style-guide.md') }
+ it { expect(blob.path).to eq('files/links/ruby-style-guide.md') }
+ it { expect(blob.size).to eq(31) }
+ it { expect(blob.mode).to eq("120000") }
+ end
+
+ context 'file symlink to binary' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6',
+ 'files/links/touch'
+ )
+ end
+
+ it { expect(blob.name).to eq('touch') }
+ it { expect(blob.path).to eq('files/links/touch') }
+ it { expect(blob.size).to eq(20) }
+ it { expect(blob.mode).to eq("120000") }
+ end
+ end
+
+ describe :commit do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ let(:commit_options) do
+ {
+ file: {
+ content: 'Lorem ipsum...',
+ path: 'documents/story.txt'
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Wow such commit',
+ branch: 'fix-mode'
+ }
+ }
+ end
+
+ let(:commit_sha) { Gitlab::Git::Blob.commit(repository, commit_options) }
+ let(:commit) { repository.lookup(commit_sha) }
+
+ it 'should add file with commit' do
+ # Commit message valid
+ expect(commit.message).to eq('Wow such commit')
+
+ tree = commit.tree.to_a.find { |tree| tree[:name] == 'documents' }
+
+ # Directory was created
+ expect(tree[:type]).to eq(:tree)
+
+ # File was created
+ expect(repository.lookup(tree[:oid]).first[:name]).to eq('story.txt')
+ end
+
+ describe "ref updating" do
+ it 'creates a commit but does not udate a ref' do
+ commit_opts = commit_options.tap{ |opts| opts[:commit][:update_ref] = false}
+ commit_sha = Gitlab::Git::Blob.commit(repository, commit_opts)
+ commit = repository.lookup(commit_sha)
+
+ # Commit message valid
+ expect(commit.message).to eq('Wow such commit')
+
+ # Does not update any related ref
+ expect(repository.lookup("fix-mode").oid).not_to eq(commit.oid)
+ expect(repository.lookup("HEAD").oid).not_to eq(commit.oid)
+ end
+ end
+
+ describe 'reject updates' do
+ it 'should reject updates' do
+ commit_options[:file][:update] = false
+ commit_options[:file][:path] = 'files/executables/ls'
+
+ expect{ commit_sha }.to raise_error('Filename already exists; update not allowed')
+ end
+ end
+
+ describe 'file modes' do
+ it 'should preserve file modes with commit' do
+ commit_options[:file][:path] = 'files/executables/ls'
+
+ entry = Gitlab::Git::Blob::find_entry_by_path(repository, commit.tree.oid, commit_options[:file][:path])
+ expect(entry[:filemode]).to eq(0100755)
+ end
+ end
+ end
+
+ describe :rename do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_NORMAL_REPO_PATH) }
+ let(:rename_options) do
+ {
+ file: {
+ path: 'NEWCONTRIBUTING.md',
+ previous_path: 'CONTRIBUTING.md',
+ content: 'Lorem ipsum...',
+ update: true
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Rename readme',
+ branch: 'master'
+ }
+ }
+ end
+
+ let(:rename_options2) do
+ {
+ file: {
+ content: 'Lorem ipsum...',
+ path: 'bin/new_executable',
+ previous_path: 'bin/executable',
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Updates toberenamed.txt',
+ branch: 'master',
+ update_ref: false
+ }
+ }
+ end
+
+ it 'maintains file permissions when renaming' do
+ mode = 0o100755
+
+ Gitlab::Git::Blob.rename(repository, rename_options2)
+
+ expect(repository.rugged.index.get(rename_options2[:file][:path])[:mode]).to eq(mode)
+ end
+
+ it 'renames the file with commit and not change file permissions' do
+ ref = rename_options[:commit][:branch]
+
+ expect(repository.rugged.index.get('CONTRIBUTING.md')).not_to be_nil
+ expect { Gitlab::Git::Blob.rename(repository, rename_options) }.to change { repository.commit_count(ref) }.by(1)
+
+ expect(repository.rugged.index.get('CONTRIBUTING.md')).to be_nil
+ expect(repository.rugged.index.get('NEWCONTRIBUTING.md')).not_to be_nil
+ end
+ end
+
+ describe :remove do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ let(:commit_options) do
+ {
+ file: {
+ path: 'README.md'
+ },
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Remove readme',
+ branch: 'feature'
+ }
+ }
+ end
+
+ let(:commit_sha) { Gitlab::Git::Blob.remove(repository, commit_options) }
+ let(:commit) { repository.lookup(commit_sha) }
+ let(:blob) { Gitlab::Git::Blob.find(repository, commit_sha, "README.md") }
+
+ it 'should remove file with commit' do
+ # Commit message valid
+ expect(commit.message).to eq('Remove readme')
+
+ # File was removed
+ expect(blob).to be_nil
+ end
+ end
+
+ describe :lfs_pointers do
+ context 'file a valid lfs pointer' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/image.jpg'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(true) }
+ it { expect(blob.lfs_oid).to eq("4206f951d2691c78aac4c0ce9f2b23580b2c92cdcc4336e1028742c0274938e0") }
+ it { expect(blob.lfs_size).to eq("19548") }
+ it { expect(blob.id).to eq("f4d76af13003d1106be7ac8c5a2a3d37ddf32c2a") }
+ it { expect(blob.name).to eq("image.jpg") }
+ it { expect(blob.path).to eq("files/lfs/image.jpg") }
+ it { expect(blob.size).to eq(130) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ describe 'file an invalid lfs pointer' do
+ context 'with correct version header but incorrect size and oid' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/archive-invalid.tar'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(false) }
+ it { expect(blob.lfs_oid).to eq(nil) }
+ it { expect(blob.lfs_size).to eq(nil) }
+ it { expect(blob.id).to eq("f8a898db217a5a85ed8b3d25b34c1df1d1094c46") }
+ it { expect(blob.name).to eq("archive-invalid.tar") }
+ it { expect(blob.path).to eq("files/lfs/archive-invalid.tar") }
+ it { expect(blob.size).to eq(43) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'with correct version header and size but incorrect size and oid' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/picture-invalid.png'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(false) }
+ it { expect(blob.lfs_oid).to eq(nil) }
+ it { expect(blob.lfs_size).to eq("1575078") }
+ it { expect(blob.id).to eq("5ae35296e1f95c1ef9feda1241477ed29a448572") }
+ it { expect(blob.name).to eq("picture-invalid.png") }
+ it { expect(blob.path).to eq("files/lfs/picture-invalid.png") }
+ it { expect(blob.size).to eq(57) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+
+ context 'with correct version header and size but invalid size and oid' do
+ let(:blob) do
+ Gitlab::Git::Blob.find(
+ repository,
+ '33bcff41c232a11727ac6d660bd4b0c2ba86d63d',
+ 'files/lfs/file-invalid.zip'
+ )
+ end
+
+ it { expect(blob.lfs_pointer?).to eq(false) }
+ it { expect(blob.lfs_oid).to eq(nil) }
+ it { expect(blob.lfs_size).to eq(nil) }
+ it { expect(blob.id).to eq("d831981bd876732b85a1bcc6cc01210c9f36248f") }
+ it { expect(blob.name).to eq("file-invalid.zip") }
+ it { expect(blob.path).to eq("files/lfs/file-invalid.zip") }
+ it { expect(blob.size).to eq(60) }
+ it { expect(blob.mode).to eq("100644") }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
new file mode 100644
index 00000000000..78234b396c5
--- /dev/null
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -0,0 +1,31 @@
+require "spec_helper"
+
+describe Gitlab::Git::Branch, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ subject { repository.branches }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#size' do
+ subject { super().size }
+ it { is_expected.to eq(SeedRepo::Repo::BRANCHES.size) }
+ end
+
+ describe 'first branch' do
+ let(:branch) { repository.branches.first }
+
+ it { expect(branch.name).to eq(SeedRepo::Repo::BRANCHES.first) }
+ it { expect(branch.dereferenced_target.sha).to eq("0b4bc9a49b562e85de7cc9e834518ea6828729b9") }
+ end
+
+ describe 'master branch' do
+ let(:branch) do
+ repository.branches.find { |branch| branch.name == 'master' }
+ end
+
+ it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
+ end
+
+ it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
+end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
new file mode 100644
index 00000000000..e1be6784c20
--- /dev/null
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -0,0 +1,408 @@
+require "spec_helper"
+
+describe Gitlab::Git::Commit, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:commit) { Gitlab::Git::Commit.find(repository, SeedRepo::Commit::ID) }
+ let(:rugged_commit) do
+ repository.rugged.lookup(SeedRepo::Commit::ID)
+ end
+
+ describe "Commit info" do
+ before do
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+
+ @committer = {
+ email: 'mike@smith.com',
+ name: "Mike Smith",
+ time: Time.now
+ }
+
+ @author = {
+ email: 'john@smith.com',
+ name: "John Smith",
+ time: Time.now
+ }
+
+ @parents = [repo.head.target]
+ @gitlab_parents = @parents.map { |c| Gitlab::Git::Commit.decorate(c) }
+ @tree = @parents.first.tree
+
+ sha = Rugged::Commit.create(
+ repo,
+ author: @author,
+ committer: @committer,
+ tree: @tree,
+ parents: @parents,
+ message: "Refactoring specs",
+ update_ref: "HEAD"
+ )
+
+ @raw_commit = repo.lookup(sha)
+ @commit = Gitlab::Git::Commit.new(@raw_commit)
+ end
+
+ it { expect(@commit.short_id).to eq(@raw_commit.oid[0..10]) }
+ it { expect(@commit.id).to eq(@raw_commit.oid) }
+ it { expect(@commit.sha).to eq(@raw_commit.oid) }
+ it { expect(@commit.safe_message).to eq(@raw_commit.message) }
+ it { expect(@commit.created_at).to eq(@raw_commit.author[:time]) }
+ it { expect(@commit.date).to eq(@raw_commit.committer[:time]) }
+ 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.different_committer?).to be_truthy }
+ it { expect(@commit.parents).to eq(@gitlab_parents) }
+ it { expect(@commit.parent_id).to eq(@parents.first.oid) }
+ it { expect(@commit.no_commit_message).to eq("--no commit message") }
+ it { expect(@commit.tree).to eq(@tree) }
+
+ after do
+ # Erase the new commit so other tests get the original repo
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+ repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
+ 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
+ )
+ end
+
+ it "should return valid commit" do
+ expect(Gitlab::Git::Commit.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')
+ 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
+ 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
+ end
+
+ it "should return nil for nonexisting ids" do
+ expect(Gitlab::Git::Commit.find(repository, "+123_4532530XYZ")).to be_nil
+ end
+
+ context 'with broken repo' do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_BROKEN_REPO_PATH) }
+
+ it 'returns nil' do
+ expect(Gitlab::Git::Commit.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') }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::LastCommit::ID) }
+ end
+ end
+
+ context 'path' do
+ subject { Gitlab::Git::Commit.last_for_path(repository, 'master', 'files/ruby') }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::Commit::ID) }
+ end
+ end
+
+ context 'ref + path' do
+ subject { Gitlab::Git::Commit.last_for_path(repository, SeedRepo::Commit::ID, 'encoding') }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::BigCommit::ID) }
+ end
+ end
+ end
+
+ describe "where" do
+ context 'path is empty string' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'master',
+ path: '',
+ limit: 10
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+ it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ end
+
+ context 'path is nil' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'master',
+ path: nil,
+ limit: 10
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+ it { is_expected.to include(SeedRepo::EmptyCommit::ID) }
+ end
+
+ context 'ref is branch name' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'master',
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("d14d6c0abdd253381df51a723d58691b2ee1ab08") }
+ it { is_expected.not_to include("eb49186cfa5c4338011f5f590fac11bd66c5c631") }
+ end
+
+ context 'ref is commit id' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: "874797c3a73b60d2187ed6e2fcabd289ff75171e",
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("2f63565e7aac07bcdadb654e253078b727143ec4") }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ end
+
+ context 'ref is tag' do
+ subject do
+ commits = Gitlab::Git::Commit.where(
+ repo: repository,
+ ref: 'v1.0.0',
+ path: 'files',
+ limit: 3,
+ offset: 1
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 3 elements' do
+ expect(subject.size).to eq(3)
+ end
+ it { is_expected.to include("874797c3a73b60d2187ed6e2fcabd289ff75171e") }
+ it { is_expected.not_to include(SeedRepo::Commit::ID) }
+ end
+ end
+
+ describe :between do
+ subject do
+ commits = Gitlab::Git::Commit.between(repository, SeedRepo::Commit::PARENT_ID, SeedRepo::Commit::ID)
+ commits.map { |c| c.id }
+ end
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+ it { is_expected.to include(SeedRepo::Commit::ID) }
+ it { is_expected.not_to include(SeedRepo::FirstCommit::ID) }
+ end
+
+ describe :find_all do
+ context 'max_count' do
+ subject do
+ commits = Gitlab::Git::Commit.find_all(
+ repository,
+ max_count: 50
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 31 elements' do
+ expect(subject.size).to eq(33)
+ 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 }
+ 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) }
+ end
+
+ context 'contains feature + max_count' do
+ subject do
+ commits = Gitlab::Git::Commit.find_all(
+ repository,
+ contains: 'feature',
+ max_count: 7
+ )
+
+ commits.map { |c| c.id }
+ end
+
+ it 'has 7 elements' do
+ expect(subject.size).to eq(7)
+ 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) }
+ end
+ end
+ end
+
+ describe :init_from_rugged do
+ let(:gitlab_commit) { Gitlab::Git::Commit.new(rugged_commit) }
+ subject { gitlab_commit }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(SeedRepo::Commit::ID) }
+ end
+ end
+
+ describe :init_from_hash do
+ let(:commit) { Gitlab::Git::Commit.new(sample_commit_hash) }
+ subject { commit }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.to eq(sample_commit_hash[:id])}
+ end
+
+ describe '#message' do
+ subject { super().message }
+ it { is_expected.to eq(sample_commit_hash[:message])}
+ end
+ end
+
+ describe :stats do
+ subject { commit.stats }
+
+ describe '#additions' do
+ subject { super().additions }
+ it { is_expected.to eq(11) }
+ end
+
+ describe '#deletions' do
+ subject { super().deletions }
+ it { is_expected.to eq(6) }
+ end
+ end
+
+ describe :to_diff do
+ subject { commit.to_diff }
+
+ it { is_expected.not_to include "From #{SeedRepo::Commit::ID}" }
+ it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
+ end
+
+ describe :has_zero_stats? do
+ it { expect(commit.has_zero_stats?).to eq(false) }
+ end
+
+ describe :to_patch do
+ subject { commit.to_patch }
+
+ it { is_expected.to include "From #{SeedRepo::Commit::ID}" }
+ it { is_expected.to include 'diff --git a/files/ruby/popen.rb b/files/ruby/popen.rb'}
+ end
+
+ describe :to_hash do
+ let(:hash) { commit.to_hash }
+ subject { hash }
+
+ it { is_expected.to be_kind_of Hash }
+
+ describe '#keys' do
+ subject { super().keys.sort }
+ it { is_expected.to match(sample_commit_hash.keys.sort) }
+ end
+ end
+
+ describe :diffs do
+ subject { commit.diffs }
+
+ it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
+ it { expect(subject.count).to eq(2) }
+ it { expect(subject.first).to be_kind_of Gitlab::Git::Diff }
+ end
+
+ describe :ref_names do
+ let(:commit) { Gitlab::Git::Commit.find(repository, 'master') }
+ subject { commit.ref_names(repository) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+ it { is_expected.to include("master") }
+ it { is_expected.not_to include("feature") }
+ end
+
+ def sample_commit_hash
+ {
+ author_email: "dmitriy.zaporozhets@gmail.com",
+ author_name: "Dmitriy Zaporozhets",
+ authored_date: "2012-02-27 20:51:12 +0200",
+ committed_date: "2012-02-27 20:51:12 +0200",
+ committer_email: "dmitriy.zaporozhets@gmail.com",
+ committer_name: "Dmitriy Zaporozhets",
+ id: SeedRepo::Commit::ID,
+ message: "tree css fixes",
+ parent_ids: ["874797c3a73b60d2187ed6e2fcabd289ff75171e"]
+ }
+ end
+end
diff --git a/spec/lib/gitlab/git/compare_spec.rb b/spec/lib/gitlab/git/compare_spec.rb
new file mode 100644
index 00000000000..f66b68e4218
--- /dev/null
+++ b/spec/lib/gitlab/git/compare_spec.rb
@@ -0,0 +1,109 @@
+require "spec_helper"
+
+describe Gitlab::Git::Compare, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(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) }
+
+ describe :commits do
+ subject do
+ compare.commits.map(&:id)
+ end
+
+ it 'has 8 elements' do
+ expect(subject.size).to eq(8)
+ end
+
+ it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
+ it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
+
+ context 'non-existing base ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'non-existing head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'base ref is equal to head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'providing nil as base ref or head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, nil, nil) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe :diffs do
+ subject do
+ compare.diffs.map(&:new_path)
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+
+ it { is_expected.to include('files/ruby/popen.rb') }
+ it { is_expected.not_to include('LICENSE') }
+
+ context 'non-existing base ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, 'no-such-branch', SeedRepo::Commit::ID) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'non-existing head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, '1234567890') }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe :same do
+ subject do
+ compare.same
+ end
+
+ it { is_expected.to eq(false) }
+
+ context 'base ref is equal to head ref' do
+ let(:compare) { Gitlab::Git::Compare.new(repository, SeedRepo::BigCommit::ID, SeedRepo::BigCommit::ID) }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+
+ describe :commits_straight do
+ subject do
+ compare_straight.commits.map(&:id)
+ end
+
+ it 'has 8 elements' do
+ expect(subject.size).to eq(8)
+ end
+
+ it { is_expected.to include(SeedRepo::Commit::PARENT_ID) }
+ it { is_expected.not_to include(SeedRepo::BigCommit::PARENT_ID) }
+ end
+
+ describe :diffs_straight do
+ subject do
+ compare_straight.diffs.map(&:new_path)
+ end
+
+ it 'has 10 elements' do
+ expect(subject.size).to eq(10)
+ end
+
+ it { is_expected.to include('files/ruby/popen.rb') }
+ it { is_expected.not_to include('LICENSE') }
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
new file mode 100644
index 00000000000..4fa72c565ae
--- /dev/null
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -0,0 +1,460 @@
+require 'spec_helper'
+
+describe Gitlab::Git::DiffCollection, seed_helper: true do
+ subject do
+ Gitlab::Git::DiffCollection.new(
+ iterator,
+ max_files: max_files,
+ max_lines: max_lines,
+ all_diffs: all_diffs,
+ no_collapse: no_collapse
+ )
+ end
+ let(:iterator) { Array.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 }
+
+ describe '#to_a' do
+ subject { super().to_a }
+ it { is_expected.to be_kind_of ::Array }
+ end
+
+ describe :decorate! do
+ let(:file_count) { 3 }
+
+ it 'modifies the array in place' do
+ count = 0
+ subject.decorate! { |d| !d.nil? && count += 1 }
+ expect(subject.to_a).to eq([1, 2, 3])
+ expect(count).to eq(3)
+ end
+
+ it 'avoids future iterator iterations' do
+ subject.decorate! { |d| d unless d.nil? }
+
+ expect(iterator).not_to receive(:each)
+
+ subject.overflow?
+ end
+ end
+
+ context 'overflow handling' do
+ context 'adding few enough files' do
+ let(:file_count) { 3 }
+
+ context 'and few enough lines' do
+ let(:line_count) { 10 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3') }
+ end
+ it { expect(subject.size).to eq(3) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3') }
+ end
+ it { expect(subject.size).to eq(3) }
+ end
+ end
+
+ context 'and too many lines' do
+ let(:line_count) { 1000 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('0+') }
+ end
+ it { expect(subject.size).to eq(0) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3') }
+ end
+ it { expect(subject.size).to eq(3) }
+ end
+ end
+ end
+
+ context 'adding too many files' do
+ let(:file_count) { 11 }
+
+ context 'and few enough lines' do
+ let(:line_count) { 1 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('10+') }
+ end
+ it { expect(subject.size).to eq(10) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('11') }
+ end
+ it { expect(subject.size).to eq(11) }
+ end
+ end
+
+ context 'and too many lines' do
+ let(:line_count) { 30 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('3+') }
+ end
+ it { expect(subject.size).to eq(3) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('11') }
+ end
+ it { expect(subject.size).to eq(11) }
+ end
+ end
+ end
+
+ context 'adding exactly the maximum number of files' do
+ let(:file_count) { 10 }
+
+ context 'and few enough lines' do
+ let(:line_count) { 1 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('10') }
+ end
+ it { expect(subject.size).to eq(10) }
+ end
+ end
+
+ context 'adding too many bytes' do
+ let(:file_count) { 10 }
+ let(:line_length) { 5200 }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('9+') }
+ end
+ it { expect(subject.size).to eq(9) }
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { true }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('10') }
+ end
+ it { expect(subject.size).to eq(10) }
+ end
+ end
+ end
+
+ describe 'empty collection' do
+ subject { Gitlab::Git::DiffCollection.new([]) }
+
+ describe '#overflow?' do
+ subject { super().overflow? }
+ it { is_expected.to be_falsey }
+ end
+
+ describe '#empty?' do
+ subject { super().empty? }
+ it { is_expected.to be_truthy }
+ end
+
+ describe '#size' do
+ subject { super().size }
+ it { is_expected.to eq(0) }
+ end
+
+ describe '#real_size' do
+ subject { super().real_size }
+ it { is_expected.to eq('0')}
+ end
+ end
+
+ describe :each do
+ context 'when diff are too large' do
+ let(:collection) do
+ Gitlab::Git::DiffCollection.new([{ diff: 'a' * 204800 }])
+ 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))
+ end
+
+ it 'prunes diffs that are too large' do
+ diff = nil
+
+ collection.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).to eq('')
+ end
+ end
+
+ context 'when diff is quite large will collapse by default' do
+ let(:iterator) { [{ diff: 'a' * 20480 }] }
+
+ context 'when no collapse is set' do
+ let(:no_collapse) { 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))
+ end
+
+ it 'does not prune diffs' do
+ diff = nil
+
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).not_to eq('')
+ end
+ end
+
+ context 'when no collapse is unset' do
+ let(:no_collapse) { 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))
+ end
+
+ it 'prunes diffs that are quite big' do
+ diff = nil
+
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).to eq('')
+ end
+
+ context 'when go over safe limits on files' do
+ let(:iterator) { [ fake_diff(1, 1) ] * 4 }
+
+ before(:each) do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: 2, max_lines: max_lines })
+ end
+
+ it 'prunes diffs by default even little ones' do
+ subject.each_with_index do |d, i|
+ if i < 2
+ expect(d.diff).not_to eq('')
+ else # 90 lines
+ expect(d.diff).to eq('')
+ end
+ end
+ end
+ end
+
+ context 'when go over safe limits on lines' do
+ let(:iterator) do
+ [
+ fake_diff(1, 45),
+ fake_diff(1, 45),
+ fake_diff(1, 20480),
+ fake_diff(1, 1)
+ ]
+ end
+
+ before(:each) do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
+ end
+
+ it 'prunes diffs by default even little ones' do
+ subject.each_with_index do |d, i|
+ if i < 2
+ expect(d.diff).not_to eq('')
+ else # 90 lines
+ expect(d.diff).to eq('')
+ end
+ end
+ end
+ end
+
+ context 'when go over safe limits on bytes' do
+ let(:iterator) do
+ [
+ fake_diff(1, 45),
+ fake_diff(1, 45),
+ fake_diff(1, 20480),
+ fake_diff(1, 1)
+ ]
+ end
+
+ before(:each) do
+ stub_const('Gitlab::Git::DiffCollection::DEFAULT_LIMITS', { max_files: max_files, max_lines: 80 })
+ end
+
+ it 'prunes diffs by default even little ones' do
+ subject.each_with_index do |d, i|
+ if i < 2
+ expect(d.diff).not_to eq('')
+ else # > 80 bytes
+ expect(d.diff).to eq('')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when limiting is disabled' do
+ let(:all_diffs) { 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))
+ end
+
+ it 'does not prune diffs' do
+ diff = nil
+
+ subject.each do |d|
+ diff = d
+ end
+
+ expect(diff.diff).not_to eq('')
+ end
+ end
+ end
+ end
+
+ def fake_diff(line_length, line_count)
+ { 'diff' => "#{'a' * line_length}\n" * line_count }
+ end
+end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
new file mode 100644
index 00000000000..4c55532d165
--- /dev/null
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -0,0 +1,287 @@
+require "spec_helper"
+
+describe Gitlab::Git::Diff, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ before do
+ @raw_diff_hash = {
+ diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""),
+ --- a/.gitmodules
+ +++ b/.gitmodules
+ @@ -4,3 +4,6 @@
+ [submodule "gitlab-shell"]
+ \tpath = gitlab-shell
+ \turl = https://github.com/gitlabhq/gitlab-shell.git
+ +[submodule "gitlab-grack"]
+ + path = gitlab-grack
+ + url = https://gitlab.com/gitlab-org/gitlab-grack.git
+
+EOT
+ new_path: ".gitmodules",
+ old_path: ".gitmodules",
+ a_mode: '100644',
+ b_mode: '100644',
+ new_file: false,
+ renamed_file: false,
+ deleted_file: false,
+ too_large: false
+ }
+
+ @rugged_diff = repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths:
+ [".gitmodules"]).patches.first
+ end
+
+ describe '.new' do
+ context 'using a Hash' do
+ context 'with a small diff' do
+ let(:diff) { described_class.new(@raw_diff_hash) }
+
+ it 'initializes the diff' do
+ expect(diff.to_hash).to eq(@raw_diff_hash)
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ context 'using a diff that is too large' do
+ it 'prunes the diff' do
+ diff = described_class.new(diff: 'a' * 204800)
+
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+ end
+
+ context 'using a Rugged::Patch' do
+ context 'with a small diff' do
+ 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))
+ end
+
+ it 'does not prune the diff' do
+ expect(diff).not_to be_too_large
+ end
+ end
+
+ 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)
+
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ end
+ end
+
+ context 'using a collapsable diff that is too large' do
+ before do
+ # 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)
+ end
+
+ it 'prunes the diff as a large diff instead of as a collapsed diff' do
+ diff = described_class.new(@rugged_diff, collapse: true)
+
+ expect(diff.diff).to be_empty
+ expect(diff).to be_too_large
+ expect(diff).not_to be_collapsed
+ end
+ end
+
+ 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)
+
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.diff).not_to be_empty
+ end
+ end
+ end
+ end
+
+ describe 'straight diffs' do
+ let(:options) { { straight: true } }
+ let(:diffs) { described_class.between(repository, 'feature', 'master', options) }
+
+ it 'has the correct size' do
+ expect(diffs.size).to eq(24)
+ end
+
+ context 'diff' do
+ it 'is an instance of Diff' do
+ expect(diffs.first).to be_kind_of(described_class)
+ end
+
+ it 'has the correct new_path' do
+ expect(diffs.first.new_path).to eq('.DS_Store')
+ end
+
+ it 'has the correct diff' do
+ expect(diffs.first.diff).to include('Binary files /dev/null and b/.DS_Store differ')
+ end
+ end
+ end
+
+ describe '.between' do
+ let(:diffs) { described_class.between(repository, 'feature', 'master') }
+ subject { diffs }
+
+ it { is_expected.to be_kind_of Gitlab::Git::DiffCollection }
+
+ describe '#size' do
+ subject { super().size }
+
+ it { is_expected.to eq(1) }
+ end
+
+ context 'diff' do
+ subject { diffs.first }
+
+ it { is_expected.to be_kind_of described_class }
+
+ describe '#new_path' do
+ subject { super().new_path }
+
+ it { is_expected.to eq('files/ruby/feature.rb') }
+ end
+
+ describe '#diff' do
+ subject { super().diff }
+
+ it { is_expected.to include '+class Feature' }
+ end
+ end
+ end
+
+ describe '.filter_diff_options' do
+ let(:options) { { max_size: 100, invalid_opt: true } }
+
+ context "without default options" do
+ let(:filtered_options) { described_class.filter_diff_options(options) }
+
+ it "should filter invalid options" do
+ expect(filtered_options).not_to have_key(:invalid_opt)
+ end
+ end
+
+ context "with default options" do
+ let(:filtered_options) do
+ default_options = { max_size: 5, bad_opt: 1, ignore_whitespace: true }
+ described_class.filter_diff_options(options, default_options)
+ end
+
+ it "should filter invalid options" do
+ expect(filtered_options).not_to have_key(:invalid_opt)
+ expect(filtered_options).not_to have_key(:bad_opt)
+ end
+
+ it "should merge with default options" do
+ expect(filtered_options).to have_key(:ignore_whitespace)
+ end
+
+ it "should override default options" do
+ expect(filtered_options).to have_key(:max_size)
+ expect(filtered_options[:max_size]).to eq(100)
+ end
+ end
+ end
+
+ describe '#submodule?' do
+ before do
+ commit = repository.lookup('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ @diffs = commit.parents[0].diff(commit).patches
+ end
+
+ it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) }
+ it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) }
+ end
+
+ describe '#line_count' do
+ it 'returns the correct number of lines' do
+ diff = described_class.new(@rugged_diff)
+
+ expect(diff.line_count).to eq(9)
+ end
+ end
+
+ describe '#too_large?' do
+ it 'returns true for a diff that is too large' do
+ diff = described_class.new(diff: 'a' * 204800)
+
+ expect(diff.too_large?).to eq(true)
+ end
+
+ it 'returns false for a diff that is small enough' do
+ diff = described_class.new(diff: 'a')
+
+ expect(diff.too_large?).to eq(false)
+ end
+
+ 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!
+
+ expect(diff.too_large?).to eq(true)
+ end
+ end
+
+ describe '#collapsed?' do
+ it 'returns false by default even on quite big diff' do
+ diff = described_class.new(diff: 'a' * 20480)
+
+ expect(diff).not_to be_collapsed
+ end
+
+ it 'returns false by default for a diff that is small enough' do
+ diff = described_class.new(diff: 'a')
+
+ expect(diff).not_to be_collapsed
+ end
+
+ it 'returns true for a diff that was explicitly marked as being collapsed' do
+ diff = described_class.new(diff: 'a')
+
+ diff.prune_collapsed_diff!
+
+ expect(diff).to be_collapsed
+ end
+ end
+
+ describe '#collapsible?' do
+ it 'returns true for a diff that is quite large' do
+ diff = described_class.new(diff: 'a' * 20480)
+
+ expect(diff).to be_collapsible
+ end
+
+ it 'returns false for a diff that is small enough' do
+ diff = described_class.new(diff: 'a')
+
+ expect(diff).not_to be_collapsible
+ end
+ end
+
+ describe '#prune_collapsed_diff!' do
+ it 'prunes the diff' do
+ diff = described_class.new(diff: "foo\nbar")
+
+ diff.prune_collapsed_diff!
+
+ expect(diff.diff).to eq('')
+ expect(diff.line_count).to eq(0)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/git/encoding_helper_spec.rb
new file mode 100644
index 00000000000..ab8a0b9fba9
--- /dev/null
+++ b/spec/lib/gitlab/git/encoding_helper_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Gitlab::Git::EncodingHelper do
+ let(:ext_class) { Class.new { extend EncodingHelper } }
+ let(:binary_string) { File.join(SEED_REPOSITORY_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
+ 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/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
new file mode 100644
index 00000000000..b99d863a8f6
--- /dev/null
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -0,0 +1,1184 @@
+require "spec_helper"
+
+describe Gitlab::Git::Repository, seed_helper: true do
+ include EncodingHelper
+
+ let(:repository) { Gitlab::Git::Repository.new(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) }
+ end
+
+ describe "#discover_default_branch" do
+ let(:master) { 'master' }
+ let(:feature) { 'feature' }
+ let(:feature2) { 'feature2' }
+
+ it "returns 'master' when master exists" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
+ expect(repository.discover_default_branch).to eq('master')
+ end
+
+ it "returns non-master when master exists but default branch is set to something else" do
+ File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/feature')
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, master])
+ expect(repository.discover_default_branch).to eq('feature')
+ File.write(File.join(repository.path, 'HEAD'), 'ref: refs/heads/master')
+ end
+
+ it "returns a non-master branch when only one exists" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature])
+ expect(repository.discover_default_branch).to eq('feature')
+ end
+
+ it "returns a non-master branch when more than one exists and master does not" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([feature, feature2])
+ expect(repository.discover_default_branch).to eq('feature')
+ end
+
+ it "returns nil when no branch exists" do
+ expect(repository).to receive(:branch_names).at_least(:once).and_return([])
+ expect(repository.discover_default_branch).to be_nil
+ end
+ end
+
+ describe :branch_names do
+ subject { repository.branch_names }
+
+ it 'has SeedRepo::Repo::BRANCHES.size elements' do
+ expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size)
+ end
+ it { is_expected.to include("master") }
+ it { is_expected.not_to include("branch-from-space") }
+ 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
+
+ describe '#last' do
+ subject { super().last }
+ it { is_expected.to eq("v1.2.1") }
+ end
+ it { is_expected.to include("v1.0.0") }
+ it { is_expected.not_to include("v5.0.0") }
+ end
+
+ shared_examples 'archive check' do |extenstion|
+ it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) }
+ it { expect(metadata['ArchivePath']).to end_with extenstion }
+ end
+
+ describe :archive do
+ let(:metadata) { repository.archive_metadata('master', '/tmp') }
+
+ it_should_behave_like 'archive check', '.tar.gz'
+ end
+
+ describe :archive_zip do
+ let(:metadata) { repository.archive_metadata('master', '/tmp', 'zip') }
+
+ it_should_behave_like 'archive check', '.zip'
+ end
+
+ describe :archive_bz2 do
+ let(:metadata) { repository.archive_metadata('master', '/tmp', 'tbz2') }
+
+ it_should_behave_like 'archive check', '.tar.bz2'
+ end
+
+ describe :archive_fallback do
+ let(:metadata) { repository.archive_metadata('master', '/tmp', 'madeup') }
+
+ it_should_behave_like 'archive check', '.tar.gz'
+ end
+
+ describe :size do
+ subject { repository.size }
+
+ it { is_expected.to be < 2 }
+ end
+
+ describe :has_commits? do
+ it { expect(repository.has_commits?).to be_truthy }
+ end
+
+ describe :empty? do
+ it { expect(repository.empty?).to be_falsey }
+ end
+
+ describe :bare? 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 }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#first' do
+ subject { super().first }
+ it { is_expected.to eq('feature') }
+ end
+
+ describe '#last' do
+ subject { super().last }
+ it { is_expected.to eq('v1.2.1') }
+ end
+ end
+
+ describe :search_files do
+ let(:results) { repository.search_files('rails', 'master') }
+ subject { results }
+
+ it { is_expected.to be_kind_of Array }
+
+ describe '#first' do
+ subject { super().first }
+ it { is_expected.to be_kind_of Gitlab::Git::BlobSnippet }
+ end
+
+ context 'blob result' do
+ subject { results.first }
+
+ describe '#ref' do
+ subject { super().ref }
+ it { is_expected.to eq('master') }
+ end
+
+ describe '#filename' do
+ subject { super().filename }
+ it { is_expected.to eq('CHANGELOG') }
+ end
+
+ describe '#startline' do
+ subject { super().startline }
+ it { is_expected.to eq(35) }
+ end
+
+ describe '#data' do
+ subject { super().data }
+ it { is_expected.to include "Ability to filter by multiple labels" }
+ end
+ end
+ end
+
+ context :submodules do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ context 'where repo has submodules' do
+ let(:submodules) { repository.submodules('master') }
+ let(:submodule) { submodules.first }
+
+ it { expect(submodules).to be_kind_of Hash }
+ it { expect(submodules.empty?).to be_falsey }
+
+ it 'should have valid data' do
+ expect(submodule).to eq([
+ "six", {
+ "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d",
+ "path" => "six",
+ "url" => "git://github.com/randx/six.git"
+ }
+ ])
+ end
+
+ it 'should handle nested submodules correctly' do
+ nested = submodules['nested/six']
+ expect(nested['path']).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['url']).to eq('git://github.com/randx/six.git')
+ expect(nested['id']).to eq('24fb71c79fcabc63dfd8832b12ee3bf2bf06b196')
+ end
+
+ it 'should not have an entry for an invalid submodule' do
+ expect(submodules).not_to have_key('invalid/path')
+ end
+
+ it 'should not have an entry for an uncommited submodule dir' do
+ submodules = repository.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')
+
+ expect(submodules.first).to eq([
+ "six", {
+ "id" => "409f37c4f05865e4fb208c771485f211a22c4c2d",
+ "path" => "six",
+ "url" => "git://github.com/randx/six.git"
+ }
+ ])
+ end
+ end
+
+ context 'where repo doesn\'t have submodules' do
+ let(:submodules) { repository.submodules('6d39438') }
+ it 'should return an empty hash' do
+ expect(submodules).to be_empty
+ end
+ end
+ 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(TEST_NORMAL_REPO_PATH, "CHANGELOG")
+ untracked_path = File.join(TEST_NORMAL_REPO_PATH, "UNTRACKED")
+ tracked_path = File.join(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(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
+ end
+ end
+
+ describe "#checkout" do
+ new_branch = "foo_branch"
+
+ context "-b" do
+ before(:all) do
+ @normal_repo = Gitlab::Git::Repository.new(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
+ 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(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(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(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(TEST_NORMAL_REPO_PATH)
+ ensure_seeds
+ end
+ end
+ end
+ end
+
+ describe "#delete_branch" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.delete_branch("feature")
+ end
+
+ it "should remove the branch from the repo" 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
+ end
+ end
+
+ describe "#create_branch" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ end
+
+ it "should create a new branch" do
+ expect(@repo.create_branch('new_branch', 'master')).not_to be_nil
+ end
+
+ it "should create a new branch with the right name" do
+ expect(@repo.create_branch('another_branch', 'master').name).to eq('another_branch')
+ end
+
+ 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")
+ 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")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#remote_names" do
+ let(:remotes) { repository.remote_names }
+
+ it "should have one entry: 'origin'" do
+ expect(remotes.size).to eq(1)
+ expect(remotes.first).to eq("origin")
+ end
+ end
+
+ describe "#refs_hash" do
+ let(:refs) { repository.refs_hash }
+
+ it "should have as many entries as branches and tags" do
+ expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
+ # We flatten in case a commit is pointed at by more than one branch and/or tag
+ expect(refs.values.flatten.size).to eq(expected_refs.size)
+ end
+ end
+
+ describe "#remote_delete" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.remote_delete("expendable")
+ end
+
+ it "should remove the remote" do
+ expect(@repo.rugged.remotes).not_to include("expendable")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#remote_add" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.remote_add("new_remote", SeedHelper::GITLAB_URL)
+ end
+
+ it "should add the remote" do
+ expect(@repo.rugged.remotes.each_name.to_a).to include("new_remote")
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#remote_update" do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.remote_update("expendable", url: TEST_NORMAL_REPO_PATH)
+ end
+
+ it "should add the remote" do
+ expect(@repo.rugged.remotes["expendable"].url).to(
+ eq(TEST_NORMAL_REPO_PATH)
+ )
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+ end
+
+ describe "#log" do
+ commit_with_old_name = nil
+ commit_with_new_name = nil
+ rename_commit = nil
+
+ before(:all) do
+ # Add new commits so that there's a renamed file in the commit history
+ repo = Gitlab::Git::Repository.new(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)
+ end
+
+ context "where 'follow' == true" do
+ options = { ref: "master", follow: true }
+
+ context "and 'path' is a directory" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the new filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding/CHANGELOG"))
+ end
+
+ it "should follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the old filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "CHANGELOG"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_new_name)
+ end
+ end
+
+ context "unknown ref" do
+ let(:log_commits) { repository.log(options.merge(ref: 'unknown')) }
+
+ it "should return empty" do
+ expect(log_commits).to eq([])
+ end
+ end
+ end
+
+ context "where 'follow' == false" do
+ options = { follow: false }
+
+ context "and 'path' is a directory" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the new filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding/CHANGELOG"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
+ end
+
+ context "and 'path' is a file that matches the old filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "CHANGELOG"))
+ end
+
+ it "should not follow renames" do
+ expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_new_name)
+ end
+ end
+
+ context "and 'path' includes a directory that used to be a file" do
+ let(:log_commits) do
+ repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
+ end
+
+ it "should return a list of commits" do
+ expect(log_commits.size).to eq(1)
+ end
+ end
+ end
+
+ 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) }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+
+ context "with limit" do
+ let(:options) { { ref: "master", limit: 1 } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with offset" do
+ let(:options) { { ref: "master", offset: 1 } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with skip_merges" do
+ let(:options) { { ref: "master", skip_merges: true } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+
+ context "with path" do
+ let(:options) { { ref: "master", path: "encoding" } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+
+ context "with follow" do
+ let(:options) { { ref: "master", path: "encoding", follow: true } }
+
+ it { expect(commits_by_walk).to eq(commits_by_shell) }
+ end
+ end
+ end
+
+ context "where provides 'after' timestamp" do
+ options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
+
+ it "should returns commits on or after that timestamp" do
+ commits = repository.log(options)
+
+ expect(commits.size).to be > 0
+ satisfy do
+ commits.all? { |commit| commit.created_at >= options[:after] }
+ end
+ end
+ end
+
+ context "where provides 'before' timestamp" do
+ options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
+
+ it "should returns commits on or before that timestamp" do
+ commits = repository.log(options)
+
+ expect(commits.size).to be > 0
+ satisfy do
+ commits.all? { |commit| commit.created_at <= options[:before] }
+ end
+ end
+ end
+
+ after(:all) do
+ # Erase our commits so other tests get the original repo
+ repo = Gitlab::Git::Repository.new(TEST_REPO_PATH).rugged
+ repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
+ end
+ end
+
+ describe "#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)
+ end
+ end
+
+ context 'SHA and master branch' do
+ let(:sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' }
+ 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)
+ 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
+ end
+ end
+
+ context 'two branches' do
+ let(:first_branch) { 'feature' }
+ let(:second_branch) { 'master' }
+
+ it 'returns the number of commits between' do
+ expect(repository.commits_between(first_branch, second_branch).count).to eq(17)
+ end
+ end
+ end
+
+ describe '#count_commits_between' do
+ subject { repository.count_commits_between('feature', 'master') }
+
+ it { is_expected.to eq(17) }
+ end
+
+ describe "branch_names_contains" do
+ subject { repository.branch_names_contains(SeedRepo::LastCommit::ID) }
+
+ it { is_expected.to include('master') }
+ it { is_expected.not_to include('feature') }
+ it { is_expected.not_to include('fix') }
+ end
+
+ describe '#autocrlf' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.rugged.config['core.autocrlf'] = true
+ end
+
+ it 'return the value of the autocrlf option' do
+ expect(@repo.autocrlf).to be(true)
+ end
+
+ after(:all) do
+ @repo.rugged.config.delete('core.autocrlf')
+ end
+ end
+
+ describe '#autocrlf=' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ @repo.rugged.config['core.autocrlf'] = false
+ end
+
+ it 'should set the autocrlf option to the provided option' do
+ @repo.autocrlf = :input
+
+ File.open(File.join(TEST_MUTABLE_REPO_PATH, '.git', 'config')) do |config_file|
+ expect(config_file.read).to match('autocrlf = input')
+ end
+ end
+
+ after(:all) do
+ @repo.rugged.config.delete('core.autocrlf')
+ end
+ end
+
+ describe '#find_branch' do
+ it 'should return a Branch for master' do
+ branch = repository.find_branch('master')
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
+
+ it 'should handle non-existent branch' do
+ branch = repository.find_branch('this-is-garbage')
+
+ expect(branch).to eq(nil)
+ end
+
+ it 'should reload Rugged::Repository and return master' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
+
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ 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
+
+ it 'should return empty branches' do
+ expect(repository.branches).to eq([])
+ 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))
+
+ allow(invalid_ref).to receive_messages(name: 'bad-branch')
+ allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError }
+
+ allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref])
+ end
+
+ it 'returns the number of branches' do
+ expect(repository.branch_count).to eq(1)
+ end
+ end
+
+ describe '#mkdir' do
+ let(:commit_options) do
+ {
+ author: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ committer: {
+ email: 'user@example.com',
+ name: 'Test User',
+ time: Time.now
+ },
+ commit: {
+ message: 'Test message',
+ branch: 'refs/heads/fix',
+ }
+ }
+ end
+
+ def generate_diff_for_path(path)
+ "diff --git a/#{path}/.gitkeep b/#{path}/.gitkeep
+new file mode 100644
+index 0000000..e69de29
+--- /dev/null
++++ b/#{path}/.gitkeep\n"
+ end
+
+ shared_examples 'mkdir diff check' do |path, expected_path|
+ it 'creates a directory' do
+ result = repository.mkdir(path, commit_options)
+ expect(result).not_to eq(nil)
+
+ # Verify another mkdir doesn't create a directory that already exists
+ expect{ repository.mkdir(path, commit_options) }.to raise_error('Directory already exists')
+ end
+ end
+
+ describe 'creates a directory in root directory' do
+ it_should_behave_like 'mkdir diff check', 'new_dir', 'new_dir'
+ end
+
+ describe 'creates a directory in subdirectory' do
+ it_should_behave_like 'mkdir diff check', 'files/ruby/test', 'files/ruby/test'
+ end
+
+ describe 'creates a directory in subdirectory with a slash' do
+ it_should_behave_like 'mkdir diff check', '/files/ruby/test2', 'files/ruby/test2'
+ end
+
+ describe 'creates a directory in subdirectory with multiple slashes' do
+ it_should_behave_like 'mkdir diff check', '//files/ruby/test3', 'files/ruby/test3'
+ end
+
+ describe 'handles relative paths' do
+ it_should_behave_like 'mkdir diff check', 'files/ruby/../test_relative', 'files/test_relative'
+ end
+
+ describe 'creates nested directories' do
+ it_should_behave_like 'mkdir diff check', 'files/missing/test', 'files/missing/test'
+ end
+
+ it 'does not attempt to create a directory with invalid relative path' do
+ expect{ repository.mkdir('../files/missing/test', commit_options) }.to raise_error('Invalid path')
+ end
+
+ it 'does not attempt to overwrite a file' do
+ expect{ repository.mkdir('README.md', commit_options) }.to raise_error('Directory already exists as a file')
+ end
+
+ it 'does not attempt to overwrite a directory' do
+ expect{ repository.mkdir('files', commit_options) }.to raise_error('Directory already exists')
+ end
+ end
+
+ describe "#ls_files" do
+ let(:master_file_paths) { repository.ls_files("master") }
+ let(:not_existed_branch) { repository.ls_files("not_existed_branch") }
+
+ it "read every file paths of master branch" do
+ expect(master_file_paths.length).to equal(40)
+ end
+
+ it "reads full file paths of master branch" 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
+ expect(master_file_paths).not_to include("six")
+ end
+
+ it "does not include 'nil'" do
+ expect(master_file_paths).not_to include(nil)
+ end
+
+ it "returns empty array when not existed branch" do
+ expect(not_existed_branch.length).to equal(0)
+ end
+ end
+
+ describe "#copy_gitattributes" do
+ let(:attributes_path) { File.join(TEST_REPO_PATH, 'info/attributes') }
+
+ it "raises an error with invalid ref" do
+ expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
+ end
+
+ context "with no .gitattrbutes" do
+ before(:each) do
+ repository.copy_gitattributes("master")
+ end
+
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+
+ context "with .gitattrbutes" do
+ before(:each) do
+ repository.copy_gitattributes("gitattributes")
+ end
+
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
+
+ it "has the same content in info/attributes as .gitattributes" do
+ contents = File.open(attributes_path, "rb") { |f| f.read }
+ expect(contents).to eq("*.md binary\n")
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+
+ context "with updated .gitattrbutes" do
+ before(:each) do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("gitattributes-updated")
+ end
+
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
+
+ it "has the updated content in info/attributes" do
+ contents = File.read(attributes_path)
+ expect(contents).to eq("*.txt binary\n")
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+
+ context "with no .gitattrbutes in HEAD but with previous info/attributes" do
+ before(:each) do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("master")
+ end
+
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
+
+ after(:each) do
+ FileUtils.rm_rf(attributes_path)
+ end
+ end
+ end
+
+ describe '#diffable' do
+ info_dir_path = attributes_path = File.join(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
+
+ expect(repository.tag_exists?(tag)).to eq(true)
+ end
+
+ it 'returns false for a non-existing tag' do
+ expect(repository.tag_exists?('v9000')).to eq(false)
+ end
+ end
+
+ describe '#branch_exists?' do
+ it 'returns true for an existing branch' do
+ expect(repository.branch_exists?('master')).to eq(true)
+ end
+
+ it 'returns false for a non-existing branch' do
+ expect(repository.branch_exists?('kittens')).to eq(false)
+ end
+
+ it 'returns false when using an invalid branch name' do
+ expect(repository.branch_exists?('.bla')).to eq(false)
+ end
+ end
+
+ describe '#local_branches' do
+ before(:all) do
+ @repo = Gitlab::Git::Repository.new(TEST_MUTABLE_REPO_PATH)
+ end
+
+ after(:all) do
+ FileUtils.rm_rf(TEST_MUTABLE_REPO_PATH)
+ ensure_seeds
+ end
+
+ it 'returns the local branches' do
+ create_remote_branch('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
+ 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
+ rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
+ end
+
+ # Build the options hash that's passed to Rugged::Commit#create
+ def commit_options(repo, index, message)
+ options = {}
+ options[:tree] = index.write_tree(repo)
+ options[:author] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:committer] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:message] ||= message
+ options[:parents] = repo.empty? ? [] : [repo.head.target].compact
+ options[:update_ref] = "HEAD"
+
+ options
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of CHANGELOG with a single new line of text.
+ def new_commit_edit_old_file(repo)
+ oid = repo.write("I replaced the changelog with this text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(
+ repo,
+ index,
+ "Edit CHANGELOG in its original location"
+ )
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of encoding/CHANGELOG with new text.
+ def new_commit_edit_new_file(repo)
+ oid = repo.write("I'm a new changelog with different text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(repo, index, "Edit encoding/CHANGELOG")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
+ # CHANGELOG file to the encoding/ directory.
+ def new_commit_move_file(repo)
+ blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
+ file_content = repo.lookup(blob_oid).content
+ oid = repo.write(file_content, :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+ index.remove("CHANGELOG")
+
+ options = commit_options(repo, index, "Move CHANGELOG to encoding/")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+end
diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb
new file mode 100644
index 00000000000..ad469e94735
--- /dev/null
+++ b/spec/lib/gitlab/git/tag_spec.rb
@@ -0,0 +1,25 @@
+require "spec_helper"
+
+describe Gitlab::Git::Tag, seed_helper: true do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+
+ 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
+
+ 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(repository.tags.size).to eq(SeedRepo::Repo::TAGS.size) }
+end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
new file mode 100644
index 00000000000..688e2a75373
--- /dev/null
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -0,0 +1,76 @@
+require "spec_helper"
+
+describe Gitlab::Git::Tree, seed_helper: true do
+ context :repo do
+ let(:repository) { Gitlab::Git::Repository.new(TEST_REPO_PATH) }
+ let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
+
+ it { expect(tree).to be_kind_of Array }
+ it { expect(tree.empty?).to be_falsey }
+ it { expect(tree.select(&:dir?).size).to eq(2) }
+ it { expect(tree.select(&:file?).size).to eq(10) }
+ it { expect(tree.select(&:submodule?).size).to eq(2) }
+
+ describe :dir do
+ let(:dir) { tree.select(&:dir?).first }
+
+ it { expect(dir).to be_kind_of Gitlab::Git::Tree }
+ it { expect(dir.id).to eq('3c122d2b7830eca25235131070602575cf8b41a1') }
+ it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(dir.name).to eq('encoding') }
+ it { expect(dir.path).to eq('encoding') }
+
+ context :subdir do
+ let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
+
+ it { expect(subdir).to be_kind_of Gitlab::Git::Tree }
+ it { expect(subdir.id).to eq('a1e8f8d745cc87e3a9248358d9352bb7f9a0aeba') }
+ it { expect(subdir.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(subdir.name).to eq('html') }
+ it { expect(subdir.path).to eq('files/html') }
+ end
+
+ context :subdir_file do
+ let(:subdir_file) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files/ruby').first }
+
+ it { expect(subdir_file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(subdir_file.id).to eq('7e3e39ebb9b2bf433b4ad17313770fbe4051649c') }
+ it { expect(subdir_file.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(subdir_file.name).to eq('popen.rb') }
+ it { expect(subdir_file.path).to eq('files/ruby/popen.rb') }
+ end
+ end
+
+ describe :file do
+ let(:file) { tree.select(&:file?).first }
+
+ it { expect(file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(file.id).to eq('dfaa3f97ca337e20154a98ac9d0be76ddd1fcc82') }
+ it { expect(file.commit_id).to eq(SeedRepo::Commit::ID) }
+ it { expect(file.name).to eq('.gitignore') }
+ end
+
+ describe :readme do
+ let(:file) { tree.select(&:readme?).first }
+
+ it { expect(file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(file.name).to eq('README.md') }
+ end
+
+ describe :contributing do
+ let(:file) { tree.select(&:contributing?).first }
+
+ it { expect(file).to be_kind_of Gitlab::Git::Tree }
+ it { expect(file.name).to eq('CONTRIBUTING.md') }
+ end
+
+ describe :submodule do
+ let(:submodule) { tree.select(&:submodule?).first }
+
+ it { expect(submodule).to be_kind_of Gitlab::Git::Tree }
+ it { expect(submodule.id).to eq('79bceae69cb5750d6567b223597999bfa91cb3b9') }
+ it { expect(submodule.commit_id).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d') }
+ it { expect(submodule.name).to eq('gitlab-shell') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/util_spec.rb b/spec/lib/gitlab/git/util_spec.rb
new file mode 100644
index 00000000000..8d43b570e98
--- /dev/null
+++ b/spec/lib/gitlab/git/util_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Util do
+ describe :count_lines do
+ [
+ ["", 0],
+ ["foo", 1],
+ ["foo\n", 1],
+ ["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)
+ end
+ end
+ end
+end