require 'spec_helper'

describe Repository do
  include RepoHelpers
  TestBlob = Struct.new(:path)

  let(:project) { create(:project, :repository) }
  let(:repository) { project.repository }
  let(:broken_repository) { create(:project, :broken_storage).repository }
  let(:user) { create(:user) }
  let(:git_user) { Gitlab::Git::User.from_gitlab(user) }

  let(:message) { 'Test message' }

  let(:merge_commit) do
    merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)

    merge_commit_id = repository.merge(user,
                                       merge_request.diff_head_sha,
                                       merge_request,
                                       message)

    repository.commit(merge_commit_id)
  end

  let(:author_email) { 'user@example.org' }
  let(:author_name) { 'John Doe' }

  def expect_to_raise_storage_error
    expect { yield }.to raise_error do |exception|
      storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable]
      expect(exception.class).to be_in(storage_exceptions)
    end
  end

  describe '#branch_names_contains' do
    subject { repository.branch_names_contains(sample_commit.id) }

    it { is_expected.to include('master') }
    it { is_expected.not_to include('feature') }
    it { is_expected.not_to include('fix') }

    describe 'when storage is broken', :broken_storage  do
      it 'should raise a storage error' do
        expect_to_raise_storage_error do
          broken_repository.branch_names_contains(sample_commit.id)
        end
      end
    end
  end

  describe '#tag_names_contains' do
    subject { repository.tag_names_contains(sample_commit.id) }

    it { is_expected.to include('v1.1.0') }
    it { is_expected.not_to include('v1.0.0') }
  end

  describe 'tags_sorted_by' do
    context 'name' do
      subject { repository.tags_sorted_by('name').map(&:name) }

      it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
    end

    context 'updated' do
      let(:tag_a) { repository.find_tag('v1.0.0') }
      let(:tag_b) { repository.find_tag('v1.1.0') }

      context 'desc' do
        subject { repository.tags_sorted_by('updated_desc').map(&:name) }

        before do
          double_first = double(committed_date: Time.now)
          double_last = double(committed_date: Time.now - 1.second)

          allow(tag_a).to receive(:dereferenced_target).and_return(double_first)
          allow(tag_b).to receive(:dereferenced_target).and_return(double_last)
          allow(repository).to receive(:tags).and_return([tag_a, tag_b])
        end

        it { is_expected.to eq(['v1.0.0', 'v1.1.0']) }
      end

      context 'asc' do
        subject { repository.tags_sorted_by('updated_asc').map(&:name) }

        before do
          double_first = double(committed_date: Time.now - 1.second)
          double_last = double(committed_date: Time.now)

          allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
          allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
          allow(repository).to receive(:tags).and_return([tag_a, tag_b])
        end

        it { is_expected.to eq(['v1.1.0', 'v1.0.0']) }
      end

      context 'annotated tag pointing to a blob' do
        let(:annotated_tag_name) { 'annotated-tag' }

        subject { repository.tags_sorted_by('updated_asc').map(&:name) }

        before do
          options = { message: 'test tag message\n',
                      tagger: { name: 'John Smith', email: 'john@gmail.com' } }
          repository.rugged.tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options)

          double_first = double(committed_date: Time.now - 1.second)
          double_last = double(committed_date: Time.now)

          allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
          allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
        end

        it { is_expected.to eq(['v1.1.0', 'v1.0.0', annotated_tag_name]) }

        after do
          repository.rugged.tags.delete(annotated_tag_name)
        end
      end
    end
  end

  describe '#ref_name_for_sha' do
    it 'returns the ref' do
      allow(repository.raw_repository).to receive(:ref_name_for_sha)
        .and_return('refs/environments/production/77')

      expect(repository.ref_name_for_sha('bla', '0' * 40)).to eq 'refs/environments/production/77'
    end
  end

  describe '#ref_exists?' do
    context 'when ref exists' do
      it 'returns true' do
        expect(repository.ref_exists?('refs/heads/master')).to be true
      end
    end

    context 'when ref does not exist' do
      it 'returns false' do
        expect(repository.ref_exists?('refs/heads/non-existent')).to be false
      end
    end

    context 'when ref format is incorrect' do
      it 'returns false' do
        expect(repository.ref_exists?('refs/heads/invalid:master')).to be false
      end
    end
  end

  describe '#last_commit_for_path' do
    shared_examples 'getting last commit for path' do
      subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }

      it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }

      describe 'when storage is broken', :broken_storage  do
        it 'should raise a storage error' do
          expect_to_raise_storage_error do
            broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
          end
        end
      end
    end

    context 'when Gitaly feature last_commit_for_path is enabled' do
      it_behaves_like 'getting last commit for path'
    end

    context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
      it_behaves_like 'getting last commit for path'
    end
  end

  describe '#last_commit_id_for_path' do
    shared_examples 'getting last commit ID for path' do
      subject { repository.last_commit_id_for_path(sample_commit.id, '.gitignore') }

      it "returns last commit id for a given path" do
        is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8')
      end

      it "caches last commit id for a given path" do
        cache = repository.send(:cache)
        key = "last_commit_id_for_path:#{sample_commit.id}:#{Digest::SHA1.hexdigest('.gitignore')}"

        expect(cache).to receive(:fetch).with(key).and_return('c1acaa5')
        is_expected.to eq('c1acaa5')
      end

      describe 'when storage is broken', :broken_storage  do
        it 'should raise a storage error' do
          expect_to_raise_storage_error do
            broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
          end
        end
      end
    end

    context 'when Gitaly feature last_commit_for_path is enabled' do
      it_behaves_like 'getting last commit ID for path'
    end

    context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do
      it_behaves_like 'getting last commit ID for path'
    end
  end

  describe '#commits' do
    it 'sets follow when path is a single path' do
      expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice

      repository.commits('master', path: 'README.md')
      repository.commits('master', path: ['README.md'])
    end

    it 'does not set follow when path is multiple paths' do
      expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original

      repository.commits('master', path: ['README.md', 'CHANGELOG'])
    end

    it 'does not set follow when there are no paths' do
      expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original

      repository.commits('master')
    end
  end

  describe '#find_commits_by_message' do
    shared_examples 'finding commits by message' do
      it 'returns commits with messages containing a given string' do
        commit_ids = repository.find_commits_by_message('submodule').map(&:id)

        expect(commit_ids).to include(
          '5937ac0a7beb003549fc5fd26fc247adbce4a52e',
          '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9',
          'cfe32cf61b73a0d5e9f13e774abde7ff789b1660'
        )
        expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
      end

      it 'is case insensitive' do
        commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)

        expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
      end
    end

    context 'when Gitaly commits_by_message feature is enabled' do
      it_behaves_like 'finding commits by message'
    end

    context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do
      it_behaves_like 'finding commits by message'
    end

    describe 'when storage is broken', :broken_storage  do
      it 'should raise a storage error' do
        expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
      end
    end
  end

  describe '#blob_at' do
    context 'blank sha' do
      subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }

      it { is_expected.to be_nil }
    end
  end

  describe '#merged_to_root_ref?' do
    context 'merged branch without ff' do
      subject { repository.merged_to_root_ref?('branch-merged') }

      it { is_expected.to be_truthy }
    end

    # If the HEAD was ff then it will be false
    context 'merged with ff' do
      subject { repository.merged_to_root_ref?('improve/awesome') }

      it { is_expected.to be_truthy }
    end

    context 'not merged branch' do
      subject { repository.merged_to_root_ref?('not-merged-branch') }

      it { is_expected.to be_falsey }
    end

    context 'default branch' do
      subject { repository.merged_to_root_ref?('master') }

      it { is_expected.to be_falsey }
    end

    context 'when pre-loaded merged branches are provided' do
      using RSpec::Parameterized::TableSyntax

      where(:branch, :pre_loaded, :expected) do
        'not-merged-branch' | ['branch-merged']     | false
        'branch-merged'     | ['not-merged-branch'] | false
        'branch-merged'     | ['branch-merged']     | true
        'not-merged-branch' | ['not-merged-branch'] | false
        'master'            | ['master']            | false
      end

      with_them do
        subject { repository.merged_to_root_ref?(branch, pre_loaded) }

        it { is_expected.to eq(expected) }
      end
    end
  end

  describe '#can_be_merged?' do
    context 'mergeable branches' do
      subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') }

      it { is_expected.to be_truthy }
    end

    context 'non-mergeable branches' do
      subject { repository.can_be_merged?('bb5206fee213d983da88c47f9cf4cc6caf9c66dc', 'feature') }

      it { is_expected.to be_falsey }
    end

    context 'non merged branch' do
      subject { repository.merged_to_root_ref?('fix') }

      it { is_expected.to be_falsey }
    end

    context 'non existent branch' do
      subject { repository.merged_to_root_ref?('non_existent_branch') }

      it { is_expected.to be_nil }
    end
  end

  describe '#commit' do
    context 'when ref exists' do
      it 'returns commit object' do
        expect(repository.commit('master'))
          .to be_an_instance_of Commit
      end
    end

    context 'when ref does not exist' do
      it 'returns nil' do
        expect(repository.commit('non-existent-ref')).to be_nil
      end
    end

    context 'when ref is not valid' do
      context 'when preceding tree element exists' do
        it 'returns nil' do
          expect(repository.commit('master:ref')).to be_nil
        end
      end

      context 'when preceding tree element does not exist' do
        it 'returns nil' do
          expect(repository.commit('non-existent:ref')).to be_nil
        end
      end
    end
  end

  describe "#create_dir" do
    it "commits a change that creates a new directory" do
      expect do
        repository.create_dir(user, 'newdir',
          message: 'Create newdir', branch_name: 'master')
      end.to change { repository.commits('master').count }.by(1)

      newdir = repository.tree('master', 'newdir')
      expect(newdir.path).to eq('newdir')
    end

    context "when committing to another project" do
      let(:forked_project) { create(:project, :repository) }

      it "creates a fork and commit to the forked project" do
        expect do
          repository.create_dir(user, 'newdir',
            message: 'Create newdir', branch_name: 'patch',
            start_branch_name: 'master', start_project: forked_project)
        end.to change { repository.commits('master').count }.by(0)

        expect(repository.branch_exists?('patch')).to be_truthy
        expect(forked_project.repository.branch_exists?('patch')).to be_falsy

        newdir = repository.tree('patch', 'newdir')
        expect(newdir.path).to eq('newdir')
      end
    end

    context "when an author is specified" do
      it "uses the given email/name to set the commit's author" do
        expect do
          repository.create_dir(user, 'newdir',
            message: 'Add newdir',
            branch_name: 'master',
            author_email: author_email, author_name: author_name)
        end.to change { repository.commits('master').count }.by(1)

        last_commit = repository.commit

        expect(last_commit.author_email).to eq(author_email)
        expect(last_commit.author_name).to eq(author_name)
      end
    end
  end

  describe "#create_file" do
    it 'commits new file successfully' do
      expect do
        repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
                               message: 'Create changelog',
                               branch_name: 'master')
      end.to change { repository.commits('master').count }.by(1)

      blob = repository.blob_at('master', 'NEWCHANGELOG')

      expect(blob.data).to eq('Changelog!')
    end

    it 'creates new file and dir when file_path has a forward slash' do
      expect do
        repository.create_file(user, 'new_dir/new_file.txt', 'File!',
                               message: 'Create new_file with new_dir',
                               branch_name: 'master')
      end.to change { repository.commits('master').count }.by(1)

      expect(repository.tree('master', 'new_dir').path).to eq('new_dir')
      expect(repository.blob_at('master', 'new_dir/new_file.txt').data).to eq('File!')
    end

    it 'respects the autocrlf setting' do
      repository.create_file(user, 'hello.txt', "Hello,\r\nWorld",
                             message: 'Add hello world',
                             branch_name: 'master')

      blob = repository.blob_at('master', 'hello.txt')

      expect(blob.data).to eq("Hello,\nWorld")
    end

    context "when an author is specified" do
      it "uses the given email/name to set the commit's author" do
        expect do
          repository.create_file(user, 'NEWREADME', 'README!',
                                 message: 'Add README',
                                 branch_name: 'master',
                                 author_email: author_email,
                                 author_name: author_name)
        end.to change { repository.commits('master').count }.by(1)

        last_commit = repository.commit

        expect(last_commit.author_email).to eq(author_email)
        expect(last_commit.author_name).to eq(author_name)
      end
    end
  end

  describe "#update_file" do
    it 'updates file successfully' do
      expect do
        repository.update_file(user, 'CHANGELOG', 'Changelog!',
                               message: 'Update changelog',
                               branch_name: 'master')
      end.to change { repository.commits('master').count }.by(1)

      blob = repository.blob_at('master', 'CHANGELOG')

      expect(blob.data).to eq('Changelog!')
    end

    it 'updates filename successfully' do
      expect do
        repository.update_file(user, 'NEWLICENSE', 'Copyright!',
                                     branch_name: 'master',
                                     previous_path: 'LICENSE',
                                     message: 'Changes filename')
      end.to change { repository.commits('master').count }.by(1)

      files = repository.ls_files('master')

      expect(files).not_to include('LICENSE')
      expect(files).to include('NEWLICENSE')
    end

    context "when an author is specified" do
      it "uses the given email/name to set the commit's author" do
        expect do
          repository.update_file(user, 'README', 'Updated README!',
                                 branch_name: 'master',
                                 previous_path: 'README',
                                 message: 'Update README',
                                 author_email: author_email,
                                 author_name: author_name)
        end.to change { repository.commits('master').count }.by(1)

        last_commit = repository.commit

        expect(last_commit.author_email).to eq(author_email)
        expect(last_commit.author_name).to eq(author_name)
      end
    end
  end

  describe "#delete_file" do
    it 'removes file successfully' do
      expect do
        repository.delete_file(user, 'README',
          message: 'Remove README', branch_name: 'master')
      end.to change { repository.commits('master').count }.by(1)

      expect(repository.blob_at('master', 'README')).to be_nil
    end

    context "when an author is specified" do
      it "uses the given email/name to set the commit's author" do
        expect do
          repository.delete_file(user, 'README',
            message: 'Remove README', branch_name: 'master',
            author_email: author_email, author_name: author_name)
        end.to change { repository.commits('master').count }.by(1)

        last_commit = repository.commit

        expect(last_commit.author_email).to eq(author_email)
        expect(last_commit.author_name).to eq(author_name)
      end
    end
  end

  describe '#get_committer_and_author' do
    it 'returns the committer and author data' do
      options = repository.get_committer_and_author(user)
      expect(options[:committer][:email]).to eq(user.email)
      expect(options[:author][:email]).to eq(user.email)
    end

    context 'when the email/name are given' do
      it 'returns an object containing the email/name' do
        options = repository.get_committer_and_author(user, email: author_email, name: author_name)
        expect(options[:author][:email]).to eq(author_email)
        expect(options[:author][:name]).to eq(author_name)
      end
    end

    context 'when the email is given but the name is not' do
      it 'returns the committer as the author' do
        options = repository.get_committer_and_author(user, email: author_email)
        expect(options[:author][:email]).to eq(user.email)
        expect(options[:author][:name]).to eq(user.name)
      end
    end

    context 'when the name is given but the email is not' do
      it 'returns nil' do
        options = repository.get_committer_and_author(user, name: author_name)
        expect(options[:author][:email]).to eq(user.email)
        expect(options[:author][:name]).to eq(user.name)
      end
    end
  end

  describe "search_files_by_content" do
    let(:results) { repository.search_files_by_content('feature', 'master') }
    subject { results }

    it { is_expected.to be_an Array }

    it 'regex-escapes the query string' do
      results = repository.search_files_by_content("test\\", 'master')

      expect(results.first).not_to start_with('fatal:')
    end

    it 'properly handles an unmatched parenthesis' do
      results = repository.search_files_by_content("test(", 'master')

      expect(results.first).not_to start_with('fatal:')
    end

    it 'properly handles when query is not present' do
      results = repository.search_files_by_content('', 'master')

      expect(results).to match_array([])
    end

    it 'properly handles query when repo is empty' do
      repository = create(:project).repository
      results = repository.search_files_by_content('test', 'master')

      expect(results).to match_array([])
    end

    describe 'when storage is broken', :broken_storage  do
      it 'should raise a storage error' do
        expect_to_raise_storage_error do
          broken_repository.search_files_by_content('feature', 'master')
        end
      end
    end

    describe 'result' do
      subject { results.first }

      it { is_expected.to be_an String }
      it { expect(subject.lines[2]).to eq("master:CHANGELOG:190:  - Feature: Replace teams with group membership\n") }
    end
  end

  describe "search_files_by_name" do
    let(:results) { repository.search_files_by_name('files', 'master') }

    it 'returns result' do
      expect(results.first).to eq('files/html/500.html')
    end

    it 'properly handles when query is not present' do
      results = repository.search_files_by_name('', 'master')

      expect(results).to match_array([])
    end

    it 'properly handles query when repo is empty' do
      repository = create(:project).repository

      results = repository.search_files_by_name('test', 'master')

      expect(results).to match_array([])
    end

    describe 'when storage is broken', :broken_storage  do
      it 'should raise a storage error' do
        expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
      end
    end
  end

  describe '#fetch_ref' do
    # Setting the var here, sidesteps the stub that makes gitaly raise an error
    # before the actual test call
    set(:broken_repository) { create(:project, :broken_storage).repository }

    describe 'when storage is broken', :broken_storage  do
      it 'should raise a storage error' do
        expect_to_raise_storage_error do
          broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
        end
      end
    end
  end

  describe '#create_ref' do
    it 'redirects the call to write_ref' do
      ref, ref_path = '1', '2'

      expect(repository.raw_repository).to receive(:write_ref).with(ref_path, ref)

      repository.create_ref(ref, ref_path)
    end
  end

  describe "#changelog", :use_clean_rails_memory_store_caching do
    it 'accepts changelog' do
      expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')])

      expect(repository.changelog.path).to eq('changelog')
    end

    it 'accepts news instead of changelog' do
      expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('news')])

      expect(repository.changelog.path).to eq('news')
    end

    it 'accepts history instead of changelog' do
      expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('history')])

      expect(repository.changelog.path).to eq('history')
    end

    it 'accepts changes instead of changelog' do
      expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changes')])

      expect(repository.changelog.path).to eq('changes')
    end

    it 'is case-insensitive' do
      expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('CHANGELOG')])

      expect(repository.changelog.path).to eq('CHANGELOG')
    end
  end

  describe "#license_blob", :use_clean_rails_memory_store_caching do
    before do
      repository.delete_file(
        user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master')
    end

    it 'handles when HEAD points to non-existent ref' do
      repository.create_file(
        user, 'LICENSE', 'Copyright!',
        message: 'Add LICENSE', branch_name: 'master')

      allow(repository).to receive(:file_on_head)
        .and_raise(Rugged::ReferenceError)

      expect(repository.license_blob).to be_nil
    end

    it 'looks in the root_ref only' do
      repository.delete_file(user, 'LICENSE',
        message: 'Remove LICENSE', branch_name: 'markdown')
      repository.create_file(user, 'LICENSE',
        Licensee::License.new('mit').content,
        message: 'Add LICENSE', branch_name: 'markdown')

      expect(repository.license_blob).to be_nil
    end

    it 'detects license file with no recognizable open-source license content' do
      repository.create_file(user, 'LICENSE', 'Copyright!',
        message: 'Add LICENSE', branch_name: 'master')

      expect(repository.license_blob.path).to eq('LICENSE')
    end

    %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename|
      it "detects '#{filename}'" do
        repository.create_file(user, filename,
          Licensee::License.new('mit').content,
          message: "Add #{filename}", branch_name: 'master')

        expect(repository.license_blob.name).to eq(filename)
      end
    end
  end

  describe '#license_key', :use_clean_rails_memory_store_caching do
    before do
      repository.delete_file(user, 'LICENSE',
        message: 'Remove LICENSE', branch_name: 'master')
    end

    it 'returns nil when no license is detected' do
      expect(repository.license_key).to be_nil
    end

    it 'returns nil when the repository does not exist' do
      expect(repository).to receive(:exists?).and_return(false)

      expect(repository.license_key).to be_nil
    end

    it 'returns nil when the content is not recognizable' do
      repository.create_file(user, 'LICENSE', 'Copyright!',
        message: 'Add LICENSE', branch_name: 'master')

      expect(repository.license_key).to be_nil
    end

    it 'returns the license key' do
      repository.create_file(user, 'LICENSE',
        Licensee::License.new('mit').content,
        message: 'Add LICENSE', branch_name: 'master')

      expect(repository.license_key).to eq('mit')
    end
  end

  describe '#license' do
    before do
      repository.delete_file(user, 'LICENSE',
        message: 'Remove LICENSE', branch_name: 'master')
    end

    it 'returns nil when no license is detected' do
      expect(repository.license).to be_nil
    end

    it 'returns nil when the repository does not exist' do
      expect(repository).to receive(:exists?).and_return(false)

      expect(repository.license).to be_nil
    end

    it 'returns nil when the content is not recognizable' do
      repository.create_file(user, 'LICENSE', 'Copyright!',
        message: 'Add LICENSE', branch_name: 'master')

      expect(repository.license).to be_nil
    end

    it 'returns the license' do
      license = Licensee::License.new('mit')
      repository.create_file(user, 'LICENSE',
        license.content,
        message: 'Add LICENSE', branch_name: 'master')

      expect(repository.license).to eq(license)
    end
  end

  describe "#gitlab_ci_yml", :use_clean_rails_memory_store_caching do
    it 'returns valid file' do
      files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')]
      expect(repository.tree).to receive(:blobs).and_return(files)

      expect(repository.gitlab_ci_yml.path).to eq('.gitlab-ci.yml')
    end

    it 'returns nil if not exists' do
      expect(repository.tree).to receive(:blobs).and_return([])
      expect(repository.gitlab_ci_yml).to be_nil
    end

    it 'returns nil for empty repository' do
      allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError)
      expect(repository.gitlab_ci_yml).to be_nil
    end
  end

  describe '#add_branch' do
    let(:branch_name) { 'new_feature' }
    let(:target) { 'master' }

    subject { repository.add_branch(user, branch_name, target) }

    context 'with Gitaly enabled' do
      it "calls Gitaly's OperationService" do
        expect_any_instance_of(Gitlab::GitalyClient::OperationService)
          .to receive(:user_create_branch).with(branch_name, user, target)
          .and_return(nil)

        subject
      end

      it 'creates_the_branch' do
        expect(subject.name).to eq(branch_name)
        expect(repository.find_branch(branch_name)).not_to be_nil
      end

      context 'with a non-existing target' do
        let(:target) { 'fake-target' }

        it "returns false and doesn't create the branch" do
          expect(subject).to be(false)
          expect(repository.find_branch(branch_name)).to be_nil
        end
      end
    end

    context 'with Gitaly disabled', :skip_gitaly_mock do
      context 'when pre hooks were successful' do
        it 'runs without errors' do
          hook = double(trigger: [true, nil])
          expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook)

          expect { subject }.not_to raise_error
        end

        it 'creates the branch' do
          allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])

          expect(subject.name).to eq(branch_name)
        end

        it 'calls the after_create_branch hook' do
          expect(repository).to receive(:after_create_branch)

          subject
        end
      end

      context 'when pre hooks failed' do
        it 'gets an error' do
          allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])

          expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
        end

        it 'does not create the branch' do
          allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])

          expect { subject }.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
          expect(repository.find_branch(branch_name)).to be_nil
        end
      end
    end
  end

  describe '#find_branch' do
    it 'loads a branch with a fresh repo' do
      expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original

      2.times do
        expect(repository.find_branch('feature')).not_to be_nil
      end
    end

    it 'loads a branch with a cached repo' do
      expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original

      2.times do
        expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil
      end
    end
  end

  describe '#update_branch_with_hooks' do
    let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
    let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
    let(:updating_ref) { 'refs/heads/feature' }
    let(:target_project) { project }
    let(:target_repository) { target_project.repository }

    context 'when pre hooks were successful' do
      before do
        service = Gitlab::Git::HooksService.new
        expect(Gitlab::Git::HooksService).to receive(:new).and_return(service)
        expect(service).to receive(:execute)
          .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref)
          .and_yield(service).and_return(true)
      end

      it 'runs without errors' do
        expect do
          Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
            new_rev
          end
        end.not_to raise_error
      end

      it 'ensures the autocrlf Git option is set to :input' do
        service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository)

        expect(service).to receive(:update_autocrlf_option)

        service.with_branch('feature') { new_rev }
      end

      context "when the branch wasn't empty" do
        it 'updates the head' do
          expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)

          Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
            new_rev
          end

          expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
        end
      end

      context 'when target project does not have the commit' do
        let(:target_project) { create(:project, :empty_repo) }
        let(:old_rev) { Gitlab::Git::BLANK_SHA }
        let(:new_rev) { project.commit('feature').sha }
        let(:updating_ref) { 'refs/heads/master' }

        it 'fetch_ref and create the branch' do
          expect(target_project.repository.raw_repository).to receive(:fetch_ref)
            .and_call_original

          Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
            .with_branch(
              'master',
              start_repository: project.repository.raw_repository,
              start_branch_name: 'feature') { new_rev }

          expect(target_repository.branch_names).to contain_exactly('master')
        end
      end

      context 'when target project already has the commit' do
        let(:target_project) { create(:project, :repository) }

        it 'does not fetch_ref and just pass the commit' do
          expect(target_repository).not_to receive(:fetch_ref)

          Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository)
            .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev }
        end
      end
    end

    context 'when temporary ref failed to be created from other project' do
      let(:target_project) { create(:project, :empty_repo) }

      before do
        expect(target_project.repository.raw_repository).to receive(:run_git)
      end

      it 'raises Rugged::ReferenceError' do
        raise_reference_error = raise_error(Rugged::ReferenceError) do |err|
          expect(err.cause).to be_nil
        end

        expect do
          Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository)
            .with_branch('feature',
                         start_repository: project.repository.raw_repository,
                         &:itself)
        end.to raise_reference_error
      end
    end

    context 'when the update adds more than one commit' do
      let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' }

      it 'runs without errors' do
        # old_rev is an ancestor of new_rev
        expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)

        # old_rev is not a direct ancestor (parent) of new_rev
        expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)

        branch = 'feature-ff-target'
        repository.add_branch(user, branch, old_rev)

        expect do
          Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
            new_rev
          end
        end.not_to raise_error
      end
    end

    context 'when the update would remove commits from the target branch' do
      let(:branch) { 'master' }
      let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha }

      it 'raises an exception' do
        # The 'master' branch is NOT an ancestor of new_rev.
        expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)

        # Updating 'master' to new_rev would lose the commits on 'master' that
        # are not contained in new_rev. This should not be allowed.
        expect do
          Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do
            new_rev
          end
        end.to raise_error(Gitlab::Git::CommitError)
      end
    end

    context 'when pre hooks failed' do
      it 'gets an error' do
        allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])

        expect do
          Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do
            new_rev
          end
        end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
      end
    end

    context 'when target branch is different from source branch' do
      before do
        allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
      end

      it 'expires branch cache' do
        expect(repository).not_to receive(:expire_exists_cache)
        expect(repository).not_to receive(:expire_root_ref_cache)
        expect(repository).not_to receive(:expire_emptiness_caches)
        expect(repository).to     receive(:expire_branches_cache)

        repository.with_branch(user, 'new-feature') do
          new_rev
        end
      end
    end

    context 'when repository is empty' do
      before do
        allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
      end

      it 'expires creation and branch cache' do
        empty_repository = create(:project, :empty_repo).repository

        expect(empty_repository).to receive(:expire_exists_cache)
        expect(empty_repository).to receive(:expire_root_ref_cache)
        expect(empty_repository).to receive(:expire_emptiness_caches)
        expect(empty_repository).to receive(:expire_branches_cache)

        empty_repository.create_file(user, 'CHANGELOG', 'Changelog!',
                                     message: 'Updates file content',
                                     branch_name: 'master')
      end
    end
  end

  shared_examples 'repo exists check' do
    it 'returns true when a repository exists' do
      expect(repository.exists?).to eq(true)
    end

    it 'returns false if no full path can be constructed' do
      allow(repository).to receive(:full_path).and_return(nil)

      expect(repository.exists?).to eq(false)
    end

    context 'with broken storage', :broken_storage do
      it 'should raise a storage error' do
        expect_to_raise_storage_error { broken_repository.exists? }
      end
    end
  end

  describe '#exists?' do
    context 'when repository_exists is disabled' do
      it_behaves_like 'repo exists check'
    end

    context 'when repository_exists is enabled', :skip_gitaly_mock do
      it_behaves_like 'repo exists check'
    end
  end

  describe '#has_visible_content?' do
    before do
      # If raw_repository.has_visible_content? gets called more than once then
      # caching is broken. We don't want that.
      expect(repository.raw_repository).to receive(:has_visible_content?)
        .once
        .and_return(result)
    end

    context 'when true' do
      let(:result) { true }

      it 'returns true and caches it' do
        expect(repository.has_visible_content?).to eq(true)
        # Second call hits the cache
        expect(repository.has_visible_content?).to eq(true)
      end
    end

    context 'when false' do
      let(:result) { false }

      it 'returns false and caches it' do
        expect(repository.has_visible_content?).to eq(false)
        # Second call hits the cache
        expect(repository.has_visible_content?).to eq(false)
      end
    end
  end

  describe '#update_autocrlf_option' do
    describe 'when autocrlf is not already set to :input' do
      before do
        repository.raw_repository.autocrlf = true
      end

      it 'sets autocrlf to :input' do
        Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option)

        expect(repository.raw_repository.autocrlf).to eq(:input)
      end
    end

    describe 'when autocrlf is already set to :input' do
      before do
        repository.raw_repository.autocrlf = :input
      end

      it 'does nothing' do
        expect(repository.raw_repository).not_to receive(:autocrlf=)
          .with(:input)

        Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option)
      end
    end
  end

  describe '#empty?' do
    let(:empty_repository) { create(:project_empty_repo).repository }

    it 'returns true for an empty repository' do
      expect(empty_repository.empty?).to eq(true)
    end

    it 'returns false for a non-empty repository' do
      expect(repository.empty?).to eq(false)
    end

    it 'caches the output' do
      expect(repository.raw_repository).to receive(:empty?)
        .once
        .and_return(false)

      repository.empty?
      repository.empty?
    end
  end

  describe '#root_ref' do
    it 'returns a branch name' do
      expect(repository.root_ref).to be_an_instance_of(String)
    end

    it 'caches the output' do
      expect(repository.raw_repository).to receive(:root_ref)
        .once
        .and_return('master')

      repository.root_ref
      repository.root_ref
    end
  end

  describe '#expire_root_ref_cache' do
    it 'expires the root reference cache' do
      repository.root_ref

      expect(repository.raw_repository).to receive(:root_ref)
        .once
        .and_return('foo')

      repository.expire_root_ref_cache

      expect(repository.root_ref).to eq('foo')
    end
  end

  describe '#expire_branch_cache' do
    # This method is private but we need it for testing purposes. Sadly there's
    # no other proper way of testing caching operations.
    let(:cache) { repository.send(:cache) }

    it 'expires the cache for all branches' do
      expect(cache).to receive(:expire)
        .at_least(repository.branches.length * 2)
        .times

      repository.expire_branch_cache
    end

    it 'expires the cache for all branches when the root branch is given' do
      expect(cache).to receive(:expire)
        .at_least(repository.branches.length * 2)
        .times

      repository.expire_branch_cache(repository.root_ref)
    end

    it 'expires the cache for a specific branch' do
      expect(cache).to receive(:expire).twice

      repository.expire_branch_cache('foo')
    end
  end

  describe '#expire_emptiness_caches' do
    let(:cache) { repository.send(:cache) }

    it 'expires the caches for an empty repository' do
      allow(repository).to receive(:empty?).and_return(true)

      expect(cache).to receive(:expire).with(:empty?)
      expect(cache).to receive(:expire).with(:has_visible_content?)

      repository.expire_emptiness_caches
    end

    it 'does not expire the cache for a non-empty repository' do
      allow(repository).to receive(:empty?).and_return(false)

      expect(cache).not_to receive(:expire).with(:empty?)
      expect(cache).not_to receive(:expire).with(:has_visible_content?)

      repository.expire_emptiness_caches
    end
  end

  describe 'skip_merges option' do
    subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", limit: 100, skip_merges: true).map { |k| k.id } }

    it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') }
  end

  describe '#merge' do
    let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }

    let(:message) { 'Test \r\n\r\n message' }

    shared_examples '#merge' do
      it 'merges the code and returns the commit id' do
        expect(merge_commit).to be_present
        expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
      end

      it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
        merge_commit_id = merge(repository, user, merge_request, message)

        expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
      end

      it 'removes carriage returns from commit message' do
        merge_commit_id = merge(repository, user, merge_request, message)

        expect(repository.commit(merge_commit_id).message).to eq(message.delete("\r"))
      end
    end

    context 'with gitaly' do
      it_behaves_like '#merge'
    end

    context 'without gitaly', :skip_gitaly_mock do
      it_behaves_like '#merge'
    end

    def merge(repository, user, merge_request, message)
      repository.merge(user, merge_request.diff_head_sha, merge_request, message)
    end
  end

  describe '#ff_merge' do
    before do
      repository.add_branch(user, 'ff-target', 'feature~5')
    end

    it 'merges the code and return the commit id' do
      merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
      merge_commit_id = repository.ff_merge(user,
                                            merge_request.diff_head_sha,
                                            merge_request.target_branch,
                                            merge_request: merge_request)
      merge_commit = repository.commit(merge_commit_id)

      expect(merge_commit).to be_present
      expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
    end

    it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
      merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
      merge_commit_id = repository.ff_merge(user,
                                            merge_request.diff_head_sha,
                                            merge_request.target_branch,
                                            merge_request: merge_request)

      expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
    end
  end

  describe '#revert' do
    let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
    let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
    let(:message) { 'revert message' }

    context 'when there is a conflict' do
      it 'raises an error' do
        expect { repository.revert(user, new_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
      end
    end

    context 'when commit was already reverted' do
      it 'raises an error' do
        repository.revert(user, update_image_commit, 'master', message)

        expect { repository.revert(user, update_image_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
      end
    end

    context 'when commit can be reverted' do
      it 'reverts the changes' do
        expect(repository.revert(user, update_image_commit, 'master', message)).to be_truthy
      end
    end

    context 'reverting a merge commit' do
      it 'reverts the changes' do
        merge_commit
        expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present

        repository.revert(user, merge_commit, 'master', message)
        expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present
      end
    end
  end

  describe '#cherry_pick' do
    let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
    let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
    let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
    let(:message) { 'cherry-pick message' }

    context 'when there is a conflict' do
      it 'raises an error' do
        expect { repository.cherry_pick(user, conflict_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
      end
    end

    context 'when commit was already cherry-picked' do
      it 'raises an error' do
        repository.cherry_pick(user, pickable_commit, 'master', message)

        expect { repository.cherry_pick(user, pickable_commit, 'master', message) }.to raise_error(Gitlab::Git::Repository::CreateTreeError)
      end
    end

    context 'when commit can be cherry-picked' do
      it 'cherry-picks the changes' do
        expect(repository.cherry_pick(user, pickable_commit, 'master', message)).to be_truthy
      end
    end

    context 'cherry-picking a merge commit' do
      it 'cherry-picks the changes' do
        expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).to be_nil

        cherry_pick_commit_sha = repository.cherry_pick(user, pickable_merge, 'improve/awesome', message)
        cherry_pick_commit_message = project.commit(cherry_pick_commit_sha).message

        expect(repository.blob_at_branch('improve/awesome', 'foo/bar/.gitkeep')).not_to be_nil
        expect(cherry_pick_commit_message).to eq(message)
      end
    end
  end

  describe '#before_delete' do
    describe 'when a repository does not exist' do
      before do
        allow(repository).to receive(:exists?).and_return(false)
      end

      it 'does not flush caches that depend on repository data' do
        expect(repository).not_to receive(:expire_cache)

        repository.before_delete
      end

      it 'flushes the tags cache' do
        expect(repository).to receive(:expire_tags_cache)

        repository.before_delete
      end

      it 'flushes the branches cache' do
        expect(repository).to receive(:expire_branches_cache)

        repository.before_delete
      end

      it 'flushes the root ref cache' do
        expect(repository).to receive(:expire_root_ref_cache)

        repository.before_delete
      end

      it 'flushes the emptiness caches' do
        expect(repository).to receive(:expire_emptiness_caches)

        repository.before_delete
      end

      it 'flushes the exists cache' do
        expect(repository).to receive(:expire_exists_cache).twice

        repository.before_delete
      end
    end

    describe 'when a repository exists' do
      before do
        allow(repository).to receive(:exists?).and_return(true)
      end

      it 'flushes the tags cache' do
        expect(repository).to receive(:expire_tags_cache)

        repository.before_delete
      end

      it 'flushes the branches cache' do
        expect(repository).to receive(:expire_branches_cache)

        repository.before_delete
      end

      it 'flushes the root ref cache' do
        expect(repository).to receive(:expire_root_ref_cache)

        repository.before_delete
      end

      it 'flushes the emptiness caches' do
        expect(repository).to receive(:expire_emptiness_caches)

        repository.before_delete
      end
    end
  end

  describe '#before_change_head' do
    it 'flushes the branch cache' do
      expect(repository).to receive(:expire_branch_cache)

      repository.before_change_head
    end

    it 'flushes the root ref cache' do
      expect(repository).to receive(:expire_root_ref_cache)

      repository.before_change_head
    end
  end

  describe '#after_change_head' do
    it 'flushes the readme cache' do
      expect(repository).to receive(:expire_method_caches).with([
        :readme,
        :changelog,
        :license,
        :contributing,
        :gitignore,
        :koding,
        :gitlab_ci,
        :avatar,
        :issue_template,
        :merge_request_template
      ])

      repository.after_change_head
    end
  end

  describe '#before_push_tag' do
    it 'flushes the cache' do
      expect(repository).to receive(:expire_statistics_caches)
      expect(repository).to receive(:expire_emptiness_caches)
      expect(repository).to receive(:expire_tags_cache)

      repository.before_push_tag
    end
  end

  describe '#after_import' do
    it 'flushes and builds the cache' do
      expect(repository).to receive(:expire_content_cache)

      repository.after_import
    end
  end

  describe '#after_push_commit' do
    it 'expires statistics caches' do
      expect(repository).to receive(:expire_statistics_caches)
        .and_call_original

      expect(repository).to receive(:expire_branch_cache)
        .with('master')
        .and_call_original

      repository.after_push_commit('master')
    end
  end

  describe '#after_create_branch' do
    it 'expires the branch caches' do
      expect(repository).to receive(:expire_branches_cache)

      repository.after_create_branch
    end
  end

  describe '#after_remove_branch' do
    it 'expires the branch caches' do
      expect(repository).to receive(:expire_branches_cache)

      repository.after_remove_branch
    end
  end

  describe '#after_create' do
    it 'flushes the exists cache' do
      expect(repository).to receive(:expire_exists_cache)

      repository.after_create
    end

    it 'flushes the root ref cache' do
      expect(repository).to receive(:expire_root_ref_cache)

      repository.after_create
    end

    it 'flushes the emptiness caches' do
      expect(repository).to receive(:expire_emptiness_caches)

      repository.after_create
    end
  end

  describe "#copy_gitattributes" do
    it 'returns true with a valid ref' do
      expect(repository.copy_gitattributes('master')).to be_truthy
    end

    it 'returns false with an invalid ref' do
      expect(repository.copy_gitattributes('invalid')).to be_falsey
    end
  end

  describe '#before_remove_tag' do
    it 'flushes the tag cache' do
      expect(repository).to receive(:expire_tags_cache).and_call_original
      expect(repository).to receive(:expire_statistics_caches).and_call_original

      repository.before_remove_tag
    end
  end

  describe '#branch_count' do
    it 'returns the number of branches' do
      expect(repository.branch_count).to be_an(Integer)

      # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
      rugged_count = repository.raw_repository.rugged.branches.count

      expect(repository.branch_count).to eq(rugged_count)
    end
  end

  describe '#tag_count' do
    it 'returns the number of tags' do
      expect(repository.tag_count).to be_an(Integer)

      # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
      rugged_count = repository.raw_repository.rugged.tags.count

      expect(repository.tag_count).to eq(rugged_count)
    end
  end

  describe '#expire_branches_cache' do
    it 'expires the cache' do
      expect(repository).to receive(:expire_method_caches)
        .with(%i(branch_names branch_count has_visible_content?))
        .and_call_original

      repository.expire_branches_cache
    end
  end

  describe '#expire_tags_cache' do
    it 'expires the cache' do
      expect(repository).to receive(:expire_method_caches)
        .with(%i(tag_names tag_count))
        .and_call_original

      repository.expire_tags_cache
    end
  end

  describe '#add_tag' do
    let(:user) { build_stubbed(:user) }

    shared_examples 'adding tag' do
      context 'with a valid target' do
        it 'creates the tag' do
          repository.add_tag(user, '8.5', 'master', 'foo')

          tag = repository.find_tag('8.5')
          expect(tag).to be_present
          expect(tag.message).to eq('foo')
          expect(tag.dereferenced_target.id).to eq(repository.commit('master').id)
        end

        it 'returns a Gitlab::Git::Tag object' do
          tag = repository.add_tag(user, '8.5', 'master', 'foo')

          expect(tag).to be_a(Gitlab::Git::Tag)
        end
      end

      context 'with an invalid target' do
        it 'returns false' do
          expect(repository.add_tag(user, '8.5', 'bar', 'foo')).to be false
        end
      end
    end

    context 'when Gitaly operation_user_add_tag feature is enabled' do
      it_behaves_like 'adding tag'
    end

    context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do
      it_behaves_like 'adding tag'

      it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do
        pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', project)
        update_hook = Gitlab::Git::Hook.new('update', project)
        post_receive_hook = Gitlab::Git::Hook.new('post-receive', project)

        allow(Gitlab::Git::Hook).to receive(:new)
          .and_return(pre_receive_hook, update_hook, post_receive_hook)

        allow(pre_receive_hook).to receive(:trigger).and_call_original
        allow(update_hook).to receive(:trigger).and_call_original
        allow(post_receive_hook).to receive(:trigger).and_call_original

        tag = repository.add_tag(user, '8.5', 'master', 'foo')

        commit_sha = repository.commit('master').id
        tag_sha = tag.target

        expect(pre_receive_hook).to have_received(:trigger)
          .with(anything, anything, anything, commit_sha, anything)
        expect(update_hook).to have_received(:trigger)
          .with(anything, anything, anything, commit_sha, anything)
        expect(post_receive_hook).to have_received(:trigger)
          .with(anything, anything, anything, tag_sha, anything)
      end
    end
  end

  describe '#rm_branch' do
    shared_examples "user deleting a branch" do
      it 'removes a branch' do
        expect(repository).to receive(:before_remove_branch)
        expect(repository).to receive(:after_remove_branch)

        repository.rm_branch(user, 'feature')
      end
    end

    context 'with gitaly enabled' do
      it_behaves_like "user deleting a branch"

      context 'when pre hooks failed' do
        before do
          allow_any_instance_of(Gitlab::GitalyClient::OperationService)
            .to receive(:user_delete_branch).and_raise(Gitlab::Git::HooksService::PreReceiveError)
        end

        it 'gets an error and does not delete the branch' do
          expect do
            repository.rm_branch(user, 'feature')
          end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)

          expect(repository.find_branch('feature')).not_to be_nil
        end
      end
    end

    context 'with gitaly disabled', :skip_gitaly_mock do
      it_behaves_like "user deleting a branch"

      let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
      let(:blank_sha) { '0000000000000000000000000000000000000000' }

      context 'when pre hooks were successful' do
        it 'runs without errors' do
          expect_any_instance_of(Gitlab::Git::HooksService).to receive(:execute)
            .with(git_user, repository.raw_repository, old_rev, blank_sha, 'refs/heads/feature')

          expect { repository.rm_branch(user, 'feature') }.not_to raise_error
        end

        it 'deletes the branch' do
          allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])

          expect { repository.rm_branch(user, 'feature') }.not_to raise_error

          expect(repository.find_branch('feature')).to be_nil
        end
      end

      context 'when pre hooks failed' do
        it 'gets an error' do
          allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])

          expect do
            repository.rm_branch(user, 'feature')
          end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
        end

        it 'does not delete the branch' do
          allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])

          expect do
            repository.rm_branch(user, 'feature')
          end.to raise_error(Gitlab::Git::HooksService::PreReceiveError)
          expect(repository.find_branch('feature')).not_to be_nil
        end
      end
    end
  end

  describe '#rm_tag' do
    shared_examples 'removing tag' do
      it 'removes a tag' do
        expect(repository).to receive(:before_remove_tag)

        repository.rm_tag(build_stubbed(:user), 'v1.1.0')

        expect(repository.find_tag('v1.1.0')).to be_nil
      end
    end

    context 'when Gitaly operation_user_delete_tag feature is enabled' do
      it_behaves_like 'removing tag'
    end

    context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do
      it_behaves_like 'removing tag'
    end
  end

  describe '#avatar' do
    it 'returns nil if repo does not exist' do
      expect(repository).to receive(:file_on_head)
        .and_raise(Rugged::ReferenceError)

      expect(repository.avatar).to eq(nil)
    end

    it 'returns the first avatar file found in the repository' do
      expect(repository).to receive(:file_on_head)
        .with(:avatar)
        .and_return(double(:tree, path: 'logo.png'))

      expect(repository.avatar).to eq('logo.png')
    end

    it 'caches the output' do
      expect(repository).to receive(:file_on_head)
        .with(:avatar)
        .once
        .and_return(double(:tree, path: 'logo.png'))

      2.times { expect(repository.avatar).to eq('logo.png') }
    end
  end

  describe '#expire_exists_cache' do
    let(:cache) { repository.send(:cache) }

    it 'expires the cache' do
      expect(cache).to receive(:expire).with(:exists?)

      repository.expire_exists_cache
    end
  end

  describe "#keep_around" do
    it "does not fail if we attempt to reference bad commit" do
      expect(repository.kept_around?('abc1234')).to be_falsey
    end

    it "stores a reference to the specified commit sha so it isn't garbage collected" do
      repository.keep_around(sample_commit.id)

      expect(repository.kept_around?(sample_commit.id)).to be_truthy
    end

    it "attempting to call keep_around on truncated ref does not fail" do
      repository.keep_around(sample_commit.id)
      ref = repository.send(:keep_around_ref_name, sample_commit.id)
      path = File.join(repository.path, ref)
      # Corrupt the reference
      File.truncate(path, 0)

      expect(repository.kept_around?(sample_commit.id)).to be_falsey

      repository.keep_around(sample_commit.id)

      expect(repository.kept_around?(sample_commit.id)).to be_falsey

      File.delete(path)
    end
  end

  describe '#update_ref' do
    it 'can create a ref' do
      Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)

      expect(repository.find_branch('foobar')).not_to be_nil
    end

    it 'raises CommitError when the ref update fails' do
      expect do
        Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
      end.to raise_error(Gitlab::Git::CommitError)
    end
  end

  describe '#contribution_guide', :use_clean_rails_memory_store_caching do
    it 'returns and caches the output' do
      expect(repository).to receive(:file_on_head)
        .with(:contributing)
        .and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md'))
        .once

      2.times do
        expect(repository.contribution_guide)
          .to be_an_instance_of(Gitlab::Git::Tree)
      end
    end
  end

  describe '#gitignore', :use_clean_rails_memory_store_caching do
    it 'returns and caches the output' do
      expect(repository).to receive(:file_on_head)
        .with(:gitignore)
        .and_return(Gitlab::Git::Tree.new(path: '.gitignore'))
        .once

      2.times do
        expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree)
      end
    end
  end

  describe '#koding_yml', :use_clean_rails_memory_store_caching do
    it 'returns and caches the output' do
      expect(repository).to receive(:file_on_head)
        .with(:koding)
        .and_return(Gitlab::Git::Tree.new(path: '.koding.yml'))
        .once

      2.times do
        expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree)
      end
    end
  end

  describe '#readme', :use_clean_rails_memory_store_caching do
    context 'with a non-existing repository' do
      it 'returns nil' do
        allow(repository).to receive(:tree).with(:head).and_return(nil)

        expect(repository.readme).to be_nil
      end
    end

    context 'with an existing repository' do
      context 'when no README exists' do
        it 'returns nil' do
          allow_any_instance_of(Tree).to receive(:readme).and_return(nil)

          expect(repository.readme).to be_nil
        end
      end

      context 'when a README exists' do
        it 'returns the README' do
          expect(repository.readme).to be_an_instance_of(ReadmeBlob)
        end
      end
    end
  end

  describe '#expire_statistics_caches' do
    it 'expires the caches' do
      expect(repository).to receive(:expire_method_caches)
        .with(%i(size commit_count))

      repository.expire_statistics_caches
    end
  end

  describe '#expire_method_caches' do
    it 'expires the caches of the given methods' do
      expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme)
      expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore)

      repository.expire_method_caches(%i(readme gitignore))
    end
  end

  describe '#expire_all_method_caches' do
    it 'expires the caches of all methods' do
      expect(repository).to receive(:expire_method_caches)
        .with(Repository::CACHED_METHODS)

      repository.expire_all_method_caches
    end

    it 'all cache_method definitions are in the lists of method caches' do
      methods = repository.methods.map do |method|
        match = /^_uncached_(.*)/.match(method)
        match[1].to_sym if match
      end.compact

      expect(methods).to match_array(Repository::CACHED_METHODS + Repository::MEMOIZED_CACHED_METHODS)
    end
  end

  describe '#file_on_head' do
    context 'with a non-existing repository' do
      it 'returns nil' do
        expect(repository).to receive(:tree).with(:head).and_return(nil)

        expect(repository.file_on_head(:readme)).to be_nil
      end
    end

    context 'with a repository that has no blobs' do
      it 'returns nil' do
        expect_any_instance_of(Tree).to receive(:blobs).and_return([])

        expect(repository.file_on_head(:readme)).to be_nil
      end
    end

    context 'with an existing repository' do
      it 'returns a Gitlab::Git::Tree' do
        expect(repository.file_on_head(:readme))
          .to be_an_instance_of(Gitlab::Git::Tree)
      end
    end
  end

  describe '#head_tree' do
    context 'with an existing repository' do
      it 'returns a Tree' do
        expect(repository.head_tree).to be_an_instance_of(Tree)
      end
    end

    context 'with a non-existing repository' do
      it 'returns nil' do
        expect(repository).to receive(:head_commit).and_return(nil)

        expect(repository.head_tree).to be_nil
      end
    end
  end

  describe '#tree' do
    context 'using a non-existing repository' do
      before do
        allow(repository).to receive(:head_commit).and_return(nil)
      end

      it 'returns nil' do
        expect(repository.tree(:head)).to be_nil
      end

      it 'returns nil when using a path' do
        expect(repository.tree(:head, 'README.md')).to be_nil
      end
    end

    context 'using an existing repository' do
      it 'returns a Tree' do
        expect(repository.tree(:head)).to be_an_instance_of(Tree)
      end
    end
  end

  describe '#size' do
    context 'with a non-existing repository' do
      it 'returns 0' do
        expect(repository).to receive(:exists?).and_return(false)

        expect(repository.size).to eq(0.0)
      end
    end

    context 'with an existing repository' do
      it 'returns the repository size as a Float' do
        expect(repository.size).to be_an_instance_of(Float)
      end
    end
  end

  describe '#commit_count' do
    context 'with a non-existing repository' do
      it 'returns 0' do
        expect(repository).to receive(:root_ref).and_return(nil)

        expect(repository.commit_count).to eq(0)
      end
    end

    context 'with an existing repository' do
      it 'returns the commit count' do
        expect(repository.commit_count).to be_an(Integer)
      end
    end
  end

  describe '#commit_count_for_ref' do
    let(:project) { create :project }

    context 'with a non-existing repository' do
      it 'returns 0' do
        expect(project.repository.commit_count_for_ref('master')).to eq(0)
      end
    end

    context 'with empty repository' do
      it 'returns 0' do
        project.create_repository
        expect(project.repository.commit_count_for_ref('master')).to eq(0)
      end
    end

    context 'when searching for the root ref' do
      it 'returns the same count as #commit_count' do
        expect(repository.commit_count_for_ref(repository.root_ref)).to eq(repository.commit_count)
      end
    end
  end

  describe '#cache_method_output', :use_clean_rails_memory_store_caching do
    let(:fallback) { 10 }

    context 'with a non-existing repository' do
      let(:project) { create(:project) } # No repository

      subject do
        repository.cache_method_output(:cats, fallback: fallback) do
          repository.cats_call_stub
        end
      end

      it 'returns the fallback value' do
        expect(subject).to eq(fallback)
      end

      it 'avoids calling the original method' do
        expect(repository).not_to receive(:cats_call_stub)

        subject
      end
    end

    context 'with a method throwing a non-existing-repository error' do
      subject do
        repository.cache_method_output(:cats, fallback: fallback) do
          raise Gitlab::Git::Repository::NoRepository
        end
      end

      it 'returns the fallback value' do
        expect(subject).to eq(fallback)
      end

      it 'does not cache the data' do
        subject

        expect(repository.instance_variable_defined?(:@cats)).to eq(false)
        expect(repository.send(:cache).exist?(:cats)).to eq(false)
      end
    end

    context 'with an existing repository' do
      it 'caches the output' do
        object = double

        expect(object).to receive(:number).once.and_return(10)

        2.times do
          val = repository.cache_method_output(:cats) { object.number }

          expect(val).to eq(10)
        end

        expect(repository.send(:cache).exist?(:cats)).to eq(true)
        expect(repository.instance_variable_get(:@cats)).to eq(10)
      end
    end
  end

  describe '#refresh_method_caches' do
    it 'refreshes the caches of the given types' do
      expect(repository).to receive(:expire_method_caches)
        .with(%i(rendered_readme license_blob license_key license))

      expect(repository).to receive(:rendered_readme)
      expect(repository).to receive(:license_blob)
      expect(repository).to receive(:license_key)
      expect(repository).to receive(:license)

      repository.refresh_method_caches(%i(readme license))
    end
  end

  describe '#gitlab_ci_yml_for' do
    before do
      repository.create_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master')
    end

    context 'when there is a .gitlab-ci.yml at the commit' do
      it 'returns the content' do
        expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT')
      end
    end

    context 'when there is no .gitlab-ci.yml at the commit' do
      it 'returns nil' do
        expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil
      end
    end
  end

  describe '#route_map_for' do
    before do
      repository.create_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master')
    end

    context 'when there is a .gitlab/route-map.yml at the commit' do
      it 'returns the content' do
        expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT')
      end
    end

    context 'when there is no .gitlab/route-map.yml at the commit' do
      it 'returns nil' do
        expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil
      end
    end
  end

  describe '#ancestor?' do
    let(:commit) { repository.commit }
    let(:ancestor) { commit.parents.first }

    context 'with Gitaly enabled' do
      it 'it is an ancestor' do
        expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
      end

      it 'it is not an ancestor' do
        expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
      end

      it 'returns false on nil-values' do
        expect(repository.ancestor?(nil, commit.id)).to eq(false)
        expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
        expect(repository.ancestor?(nil, nil)).to eq(false)
      end
    end

    context 'with Gitaly disabled' do
      before do
        allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false)
        allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false)
      end

      it 'it is an ancestor' do
        expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
      end

      it 'it is not an ancestor' do
        expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
      end

      it 'returns false on nil-values' do
        expect(repository.ancestor?(nil, commit.id)).to eq(false)
        expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
        expect(repository.ancestor?(nil, nil)).to eq(false)
      end
    end
  end

  describe 'commit cache' do
    set(:project) { create(:project, :repository) }

    it 'caches based on SHA' do
      # Gets the commit oid, and warms the cache
      oid = project.commit.id

      expect(Gitlab::Git::Commit).not_to receive(:find).once

      project.commit_by(oid: oid)
    end

    it 'caches nil values' do
      expect(Gitlab::Git::Commit).to receive(:find).once

      project.commit_by(oid: '1' * 40)
      project.commit_by(oid: '1' * 40)
    end
  end

  describe '#raw_repository' do
    subject { repository.raw_repository }

    it 'returns a Gitlab::Git::Repository representation of the repository' do
      expect(subject).to be_a(Gitlab::Git::Repository)
      expect(subject.relative_path).to eq(project.disk_path + '.git')
      expect(subject.gl_repository).to eq("project-#{project.id}")
    end

    context 'with a wiki repository' do
      let(:repository) { project.wiki.repository }

      it 'creates a Gitlab::Git::Repository with the proper attributes' do
        expect(subject).to be_a(Gitlab::Git::Repository)
        expect(subject.relative_path).to eq(project.disk_path + '.wiki.git')
        expect(subject.gl_repository).to eq("wiki-#{project.id}")
      end
    end
  end
end
