repository_spec.rb 63.8 KB
Newer Older
1
# coding: utf-8
Robert Speicher's avatar
Robert Speicher committed
2 3
require "spec_helper"

4
describe Gitlab::Git::Repository, :seed_helper do
5
  include Gitlab::EncodingHelper
6
  using RSpec::Parameterized::TableSyntax
Robert Speicher's avatar
Robert Speicher committed
7

8 9 10 11 12 13 14 15 16 17 18 19 20 21
  shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method|
    it 'wraps gRPC not found error' do
      expect_any_instance_of(gitaly_client_class).to receive(gitaly_client_method)
        .and_raise(GRPC::NotFound)
      expect { subject }.to raise_error(Gitlab::Git::Repository::NoRepository)
    end

    it 'wraps gRPC unknown error' do
      expect_any_instance_of(gitaly_client_class).to receive(gitaly_client_method)
        .and_raise(GRPC::Unknown)
      expect { subject }.to raise_error(Gitlab::Git::CommandError)
    end
  end

22
  let(:mutable_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
23
  let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
24 25
  let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
  let(:repository_rugged) { Rugged::Repository.new(repository_path) }
26
  let(:storage_path) { TestEnv.repos_path }
27
  let(:user) { build(:user) }
Robert Speicher's avatar
Robert Speicher committed
28

29
  describe '.create_hooks' do
30
    let(:repo_path) { File.join(storage_path, 'hook-test.git') }
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
    let(:hooks_dir) { File.join(repo_path, 'hooks') }
    let(:target_hooks_dir) { Gitlab.config.gitlab_shell.hooks_path }
    let(:existing_target) { File.join(repo_path, 'foobar') }

    before do
      FileUtils.rm_rf(repo_path)
      FileUtils.mkdir_p(repo_path)
    end

    context 'hooks is a directory' do
      let(:existing_file) { File.join(hooks_dir, 'my-file') }

      before do
        FileUtils.mkdir_p(hooks_dir)
        FileUtils.touch(existing_file)
        described_class.create_hooks(repo_path, target_hooks_dir)
      end

      it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
      it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) }
    end

    context 'hooks is a valid symlink' do
      before do
        FileUtils.mkdir_p existing_target
        File.symlink(existing_target, hooks_dir)
        described_class.create_hooks(repo_path, target_hooks_dir)
      end

      it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
    end

    context 'hooks is a broken symlink' do
      before do
        FileUtils.rm_f(existing_target)
        File.symlink(existing_target, hooks_dir)
        described_class.create_hooks(repo_path, target_hooks_dir)
      end

      it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
    end
  end

Robert Speicher's avatar
Robert Speicher committed
74 75 76 77 78 79 80
  describe "Respond to" do
    subject { repository }

    it { is_expected.to respond_to(:root_ref) }
    it { is_expected.to respond_to(:tags) }
  end

81
  describe '#root_ref' do
82
    it 'returns UTF-8' do
83
      expect(repository.root_ref).to be_utf8
84 85
    end

86
    it 'gets the branch name from GitalyClient' do
87
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:default_branch_name)
88 89
      repository.root_ref
    end
90

91
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :default_branch_name do
92
      subject { repository.root_ref }
93
    end
94 95
  end

96
  describe '#branch_names' do
Robert Speicher's avatar
Robert Speicher committed
97 98 99 100 101
    subject { repository.branch_names }

    it 'has SeedRepo::Repo::BRANCHES.size elements' do
      expect(subject.size).to eq(SeedRepo::Repo::BRANCHES.size)
    end
102 103

    it 'returns UTF-8' do
104
      expect(subject.first).to be_utf8
105 106
    end

Robert Speicher's avatar
Robert Speicher committed
107 108
    it { is_expected.to include("master") }
    it { is_expected.not_to include("branch-from-space") }
109

110
    it 'gets the branch names from GitalyClient' do
111
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:branch_names)
112 113
      subject
    end
114

115
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branch_names
Robert Speicher's avatar
Robert Speicher committed
116 117
  end

118
  describe '#tag_names' do
Robert Speicher's avatar
Robert Speicher committed
119 120 121
    subject { repository.tag_names }

    it { is_expected.to be_kind_of Array }
122

Robert Speicher's avatar
Robert Speicher committed
123 124 125 126
    it 'has SeedRepo::Repo::TAGS.size elements' do
      expect(subject.size).to eq(SeedRepo::Repo::TAGS.size)
    end

127
    it 'returns UTF-8' do
128
      expect(subject.first).to be_utf8
129 130
    end

Robert Speicher's avatar
Robert Speicher committed
131 132 133 134 135 136
    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") }
137

138
    it 'gets the tag names from GitalyClient' do
139
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names)
140 141
      subject
    end
142

143
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names
Robert Speicher's avatar
Robert Speicher committed
144 145
  end

146 147 148
  describe '#archive_metadata' do
    let(:storage_path) { '/tmp' }
    let(:cache_key) { File.join(repository.gl_repository, SeedRepo::LastCommit::ID) }
Robert Speicher's avatar
Robert Speicher committed
149

150 151 152
    let(:append_sha) { true }
    let(:ref) { 'master' }
    let(:format) { nil }
153

154 155 156 157
    let(:expected_extension) { 'tar.gz' }
    let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
    let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
    let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
158

159
    subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) }
160

161 162
    it 'sets CommitId to the commit SHA' do
      expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
163
    end
164

165 166
    it 'sets ArchivePrefix to the expected prefix' do
      expect(metadata['ArchivePrefix']).to eq(expected_prefix)
167
    end
168

169 170 171 172
    it 'sets ArchivePath to the expected globally-unique path' do
      # This is really important from a security perspective. Think carefully
      # before changing it: https://gitlab.com/gitlab-org/gitlab-ce/issues/45689
      expect(expected_path).to include(File.join(repository.gl_repository, SeedRepo::LastCommit::ID))
Robert Speicher's avatar
Robert Speicher committed
173

174 175
      expect(metadata['ArchivePath']).to eq(expected_path)
    end
Robert Speicher's avatar
Robert Speicher committed
176

177 178 179
    context 'append_sha varies archive path and filename' do
      where(:append_sha, :ref, :expected_prefix) do
        sha = SeedRepo::LastCommit::ID
Robert Speicher's avatar
Robert Speicher committed
180

181 182 183 184 185 186 187
        true  | 'master' | "gitlab-git-test-master-#{sha}"
        true  | sha      | "gitlab-git-test-#{sha}-#{sha}"
        false | 'master' | "gitlab-git-test-master"
        false | sha      | "gitlab-git-test-#{sha}"
        nil   | 'master' | "gitlab-git-test-master-#{sha}"
        nil   | sha      | "gitlab-git-test-#{sha}"
      end
Robert Speicher's avatar
Robert Speicher committed
188

189 190 191 192 193
      with_them do
        it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) }
        it { expect(metadata['ArchivePath']).to eq(expected_path) }
      end
    end
Robert Speicher's avatar
Robert Speicher committed
194

195 196 197 198 199 200 201
    context 'format varies archive path and filename' do
      where(:format, :expected_extension) do
        nil      | 'tar.gz'
        'madeup' | 'tar.gz'
        'tbz2'   | 'tar.bz2'
        'zip'    | 'zip'
      end
Robert Speicher's avatar
Robert Speicher committed
202

203 204 205 206 207
      with_them do
        it { expect(metadata['ArchivePrefix']).to eq(expected_prefix) }
        it { expect(metadata['ArchivePath']).to eq(expected_path) }
      end
    end
Robert Speicher's avatar
Robert Speicher committed
208 209
  end

210
  describe '#size' do
Robert Speicher's avatar
Robert Speicher committed
211 212 213 214 215
    subject { repository.size }

    it { is_expected.to be < 2 }
  end

216
  describe '#empty?' do
217
    it { expect(repository).not_to be_empty }
Robert Speicher's avatar
Robert Speicher committed
218 219
  end

220
  describe '#ref_names' do
Robert Speicher's avatar
Robert Speicher committed
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    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

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
  describe '#submodule_url_for' do
    let(:ref) { 'master' }

    def submodule_url(path)
      repository.submodule_url_for(ref, path)
    end

    it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') }
    it { expect(submodule_url('nested/six')).to eq('git://github.com/randx/six.git') }
    it { expect(submodule_url('deeper/nested/six')).to eq('git://github.com/randx/six.git') }
    it { expect(submodule_url('invalid/path')).to eq(nil) }

    context 'uncommitted submodule dir' do
      let(:ref) { 'fix-existing-submodule-dir' }

      it { expect(submodule_url('submodule-existing-dir')).to eq(nil) }
    end

    context 'tags' do
      let(:ref) { 'v1.2.1' }

      it { expect(submodule_url('six')).to eq('git://github.com/randx/six.git') }
    end

261 262 263 264 265 266 267
    context 'no .gitmodules at commit' do
      let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' }

      it { expect(submodule_url('six')).to eq(nil) }
    end

    context 'no gitlink entry' do
268 269 270 271 272 273
      let(:ref) { '6d39438' }

      it { expect(submodule_url('six')).to eq(nil) }
    end
  end

274
  describe '#commit_count' do
275 276 277
    it { expect(repository.commit_count("master")).to eq(25) }
    it { expect(repository.commit_count("feature")).to eq(9) }
    it { expect(repository.commit_count("does-not-exist")).to eq(0) }
278

279 280
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do
      subject { repository.commit_count('master') }
281
    end
Robert Speicher's avatar
Robert Speicher committed
282 283
  end

284
  describe '#has_local_branches?' do
285
    context 'check for local branches' do
286 287 288
      it { expect(repository.has_local_branches?).to eq(true) }

      context 'mutable' do
289
        let(:repository) { mutable_repository }
290 291 292 293 294 295 296

        after do
          ensure_seeds
        end

        it 'returns false when there are no branches' do
          # Sanity check
297
          expect(repository.has_local_branches?).to eq(true)
298

299 300
          FileUtils.rm_rf(File.join(repository_path, 'packed-refs'))
          heads_dir = File.join(repository_path, 'refs/heads')
301 302 303
          FileUtils.rm_rf(heads_dir)
          FileUtils.mkdir_p(heads_dir)

304
          repository.expire_has_local_branches_cache
305 306 307
          expect(repository.has_local_branches?).to eq(false)
        end
      end
308 309 310 311 312 313 314 315 316 317

      context 'memoizes the value' do
        it 'returns true' do
          expect(repository).to receive(:uncached_has_local_branches?).once.and_call_original

          2.times do
            expect(repository.has_local_branches?).to eq(true)
          end
        end
      end
318 319 320
    end
  end

Robert Speicher's avatar
Robert Speicher committed
321
  describe "#delete_branch" do
322
    let(:repository) { mutable_repository }
323

324 325 326
    after do
      ensure_seeds
    end
327

328 329
    it "removes the branch from the repo" do
      branch_name = "to-be-deleted-soon"
330

331 332
      repository.create_branch(branch_name)
      expect(repository_rugged.branches[branch_name]).not_to be_nil
Robert Speicher's avatar
Robert Speicher committed
333

334 335
      repository.delete_branch(branch_name)
      expect(repository_rugged.branches[branch_name]).to be_nil
Robert Speicher's avatar
Robert Speicher committed
336 337
    end

338 339 340 341
    context "when branch does not exist" do
      it "raises a DeleteBranchError exception" do
        expect { repository.delete_branch("this-branch-does-not-exist") }.to raise_error(Gitlab::Git::Repository::DeleteBranchError)
      end
Robert Speicher's avatar
Robert Speicher committed
342 343 344 345
    end
  end

  describe "#create_branch" do
346
    let(:repository) { mutable_repository }
Robert Speicher's avatar
Robert Speicher committed
347

348 349 350
    after do
      ensure_seeds
    end
351

352 353 354
    it "should create a new branch" do
      expect(repository.create_branch('new_branch', 'master')).not_to be_nil
    end
355

356 357
    it "should create a new branch with the right name" do
      expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch')
Robert Speicher's avatar
Robert Speicher committed
358 359
    end

360 361 362
    it "should fail if we create an existing branch" do
      repository.create_branch('duplicated_branch', 'master')
      expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
Robert Speicher's avatar
Robert Speicher committed
363 364
    end

365 366
    it "should fail if we create a branch from a non existing ref" do
      expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
Robert Speicher's avatar
Robert Speicher committed
367 368 369
    end
  end

370
  describe '#delete_refs' do
371
    let(:repository) { mutable_repository }
372

373 374 375
    after do
      ensure_seeds
    end
376

377
    it 'deletes the ref' do
378
      repository.delete_refs('refs/heads/feature')
379

380
      expect(repository_rugged.references['refs/heads/feature']).to be_nil
381
    end
382

383 384
    it 'deletes all refs' do
      refs = %w[refs/heads/wip refs/tags/v1.1.0]
385
      repository.delete_refs(*refs)
386

387
      refs.each do |ref|
388
        expect(repository_rugged.references[ref]).to be_nil
389 390 391
      end
    end

392
    it 'does not fail when deleting an empty list of refs' do
393
      expect { repository.delete_refs(*[]) }.not_to raise_error
394 395
    end

396
    it 'raises an error if it failed' do
397
      expect { repository.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError)
398 399 400
    end
  end

401
  describe '#branch_names_contains_sha' do
402
    let(:head_id) { repository_rugged.head.target.oid }
403 404
    let(:new_branch) { head_id }
    let(:utf8_branch) { 'branch-é' }
405

406 407 408
    before do
      repository.create_branch(new_branch, 'master')
      repository.create_branch(utf8_branch, 'master')
409 410
    end

411 412 413
    after do
      repository.delete_branch(new_branch)
      repository.delete_branch(utf8_branch)
414 415
    end

416 417
    it 'displays that branch' do
      expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
418 419 420
    end
  end

Robert Speicher's avatar
Robert Speicher committed
421
  describe "#refs_hash" do
422
    subject { repository.refs_hash }
Robert Speicher's avatar
Robert Speicher committed
423 424 425 426

    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
427 428 429 430 431
      expect(subject.values.flatten.size).to eq(expected_refs.size)
    end

    it 'has valid commit ids as keys' do
      expect(subject.keys).to all( match(Commit::COMMIT_SHA_PATTERN) )
Robert Speicher's avatar
Robert Speicher committed
432 433 434
    end
  end

435
  describe '#fetch_repository_as_mirror' do
436 437
    let(:new_repository) do
      Gitlab::Git::Repository.new('default', 'my_project.git', '')
Robert Speicher's avatar
Robert Speicher committed
438 439
    end

440
    subject { new_repository.fetch_repository_as_mirror(repository) }
441 442

    before do
443
      Gitlab::Shell.new.create_repository('default', 'my_project')
Robert Speicher's avatar
Robert Speicher committed
444 445
    end

446
    after do
447
      Gitlab::Shell.new.remove_repository('default', 'my_project')
448 449
    end

450 451
    it 'fetches a repository as a mirror remote' do
      subject
452

453 454
      expect(refs(new_repository_path)).to eq(refs(repository_path))
    end
455

456 457 458 459
    context 'with keep-around refs' do
      let(:sha) { SeedRepo::Commit::ID }
      let(:keep_around_ref) { "refs/keep-around/#{sha}" }
      let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" }
460

461 462 463
      before do
        repository_rugged.references.create(keep_around_ref, sha, force: true)
        repository_rugged.references.create(tmp_ref, sha, force: true)
464
      end
465

466 467
      it 'includes the temporary and keep-around refs' do
        subject
468

469 470 471
        expect(refs(new_repository_path)).to include(keep_around_ref)
        expect(refs(new_repository_path)).to include(tmp_ref)
      end
472
    end
473 474

    def new_repository_path
475
      File.join(TestEnv.repos_path, new_repository.relative_path)
476
    end
477 478
  end

479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499
  describe '#fetch_remote' do
    it 'delegates to the gitaly RepositoryService' do
      ssh_auth = double(:ssh_auth)
      expected_opts = {
        ssh_auth: ssh_auth,
        forced: true,
        no_tags: true,
        timeout: described_class::GITLAB_PROJECTS_TIMEOUT,
        prune: false
      }

      expect(repository.gitaly_repository_client).to receive(:fetch_remote).with('remote-name', expected_opts)

      repository.fetch_remote('remote-name', ssh_auth: ssh_auth, forced: true, no_tags: true, prune: false)
    end

    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :fetch_remote do
      subject { repository.fetch_remote('remote-name') }
    end
  end

500 501 502 503 504 505 506 507
  describe '#find_remote_root_ref' do
    it 'gets the remote root ref from GitalyClient' do
      expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
        .to receive(:find_remote_root_ref).and_call_original

      expect(repository.find_remote_root_ref('origin')).to eq 'master'
    end

508 509 510 511
    it 'returns UTF-8' do
      expect(repository.find_remote_root_ref('origin')).to be_utf8
    end

512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530
    it 'returns nil when remote name is nil' do
      expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
        .not_to receive(:find_remote_root_ref)

      expect(repository.find_remote_root_ref(nil)).to be_nil
    end

    it 'returns nil when remote name is empty' do
      expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
        .not_to receive(:find_remote_root_ref)

      expect(repository.find_remote_root_ref('')).to be_nil
    end

    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RemoteService, :find_remote_root_ref do
      subject { repository.find_remote_root_ref('origin') }
    end
  end

Robert Speicher's avatar
Robert Speicher committed
531
  describe "#log" do
532 533
    shared_examples 'repository log' do
      let(:commit_with_old_name) do
534
        Gitlab::Git::Commit.find(repository, @commit_with_old_name_id)
535 536
      end
      let(:commit_with_new_name) do
537
        Gitlab::Git::Commit.find(repository, @commit_with_new_name_id)
538 539
      end
      let(:rename_commit) do
540
        Gitlab::Git::Commit.find(repository, @rename_commit_id)
541
      end
Robert Speicher's avatar
Robert Speicher committed
542

543
      before do
544
        # Add new commits so that there's a renamed file in the commit history
545 546 547
        @commit_with_old_name_id = new_commit_edit_old_file(repository_rugged).oid
        @rename_commit_id = new_commit_move_file(repository_rugged).oid
        @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged).oid
548
      end
Robert Speicher's avatar
Robert Speicher committed
549

550
      after do
551
        # Erase our commits so other tests get the original repo
552
        repository_rugged.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
Robert Speicher's avatar
Robert Speicher committed
553 554
      end

555 556 557 558 559 560
      context "where 'follow' == true" do
        let(:options) { { ref: "master", follow: true } }

        context "and 'path' is a directory" do
          it "does not follow renames" do
            log_commits = repository.log(options.merge(path: "encoding"))
561 562 563 564

            aggregate_failures do
              expect(log_commits).to include(commit_with_new_name)
              expect(log_commits).to include(rename_commit)
565
              expect(log_commits).not_to include(commit_with_old_name)
566
            end
567
          end
Robert Speicher's avatar
Robert Speicher committed
568 569
        end

570 571 572 573
        context "and 'path' is a file that matches the new filename" do
          context 'without offset' do
            it "follows renames" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
574

575 576 577 578 579
              aggregate_failures 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
580
            end
581 582
          end

583 584 585
          context 'with offset=1' do
            it "follows renames and skip the latest commit" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
586

587 588 589 590 591 592
              aggregate_failures do
                expect(log_commits).not_to include(commit_with_new_name)
                expect(log_commits).to include(rename_commit)
                expect(log_commits).to include(commit_with_old_name)
              end
            end
593 594
          end

595 596 597
          context 'with offset=1', 'and limit=1' do
            it "follows renames, skip the latest commit and return only one commit" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
598

599
              expect(log_commits).to contain_exactly(rename_commit)
600
            end
601 602
          end

603 604 605
          context 'with offset=1', 'and limit=2' do
            it "follows renames, skip the latest commit and return only two commits" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
606

607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629
              aggregate_failures do
                expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
              end
            end
          end

          context 'with offset=2' do
            it "follows renames and skip the latest commit" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))

              aggregate_failures do
                expect(log_commits).not_to include(commit_with_new_name)
                expect(log_commits).not_to include(rename_commit)
                expect(log_commits).to include(commit_with_old_name)
              end
            end
          end

          context 'with offset=2', 'and limit=1' do
            it "follows renames, skip the two latest commit and return only one commit" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))

              expect(log_commits).to contain_exactly(commit_with_old_name)
630
            end
631 632
          end

633 634 635
          context 'with offset=2', 'and limit=2' do
            it "follows renames, skip the two latest commit and return only one commit" do
              log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
636

637 638 639 640 641 642
              aggregate_failures do
                expect(log_commits).not_to include(commit_with_new_name)
                expect(log_commits).not_to include(rename_commit)
                expect(log_commits).to include(commit_with_old_name)
              end
            end
643 644 645
          end
        end

646 647 648
        context "and 'path' is a file that matches the old filename" do
          it "does not follow renames" do
            log_commits = repository.log(options.merge(path: "CHANGELOG"))
649 650 651

            aggregate_failures do
              expect(log_commits).not_to include(commit_with_new_name)
652
              expect(log_commits).to include(rename_commit)
653 654
              expect(log_commits).to include(commit_with_old_name)
            end
655
          end
Robert Speicher's avatar
Robert Speicher committed
656 657
        end

658 659 660
        context "unknown ref" do
          it "returns an empty array" do
            log_commits = repository.log(options.merge(ref: 'unknown'))
Robert Speicher's avatar
Robert Speicher committed
661

662
            expect(log_commits).to eq([])
663
          end
Robert Speicher's avatar
Robert Speicher committed
664 665 666
        end
      end

667 668
      context "where 'follow' == false" do
        options = { follow: false }
Robert Speicher's avatar
Robert Speicher committed
669

670 671 672 673
        context "and 'path' is a directory" do
          let(:log_commits) do
            repository.log(options.merge(path: "encoding"))
          end
Robert Speicher's avatar
Robert Speicher committed
674

675 676 677 678 679
          it "does 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
Robert Speicher's avatar
Robert Speicher committed
680 681
        end

682 683 684 685
        context "and 'path' is a file that matches the new filename" do
          let(:log_commits) do
            repository.log(options.merge(path: "encoding/CHANGELOG"))
          end
Robert Speicher's avatar
Robert Speicher committed
686

687 688 689 690 691
          it "does 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
Robert Speicher's avatar
Robert Speicher committed
692 693
        end

694 695 696 697
        context "and 'path' is a file that matches the old filename" do
          let(:log_commits) do
            repository.log(options.merge(path: "CHANGELOG"))
          end
Robert Speicher's avatar
Robert Speicher committed
698

699 700 701 702 703
          it "does 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
Robert Speicher's avatar
Robert Speicher committed
704 705
        end

706 707 708 709 710 711 712 713
        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 "returns a list of commits" do
            expect(log_commits.size).to eq(1)
          end
Robert Speicher's avatar
Robert Speicher committed
714 715 716
        end
      end

717 718
      context "where provides 'after' timestamp" do
        options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
Robert Speicher's avatar
Robert Speicher committed
719

720 721 722 723 724 725 726
        it "should returns commits on or after that timestamp" do
          commits = repository.log(options)

          expect(commits.size).to be > 0
          expect(commits).to satisfy do |commits|
            commits.all? { |commit| commit.committed_date >= options[:after] }
          end
Robert Speicher's avatar
Robert Speicher committed
727 728 729
        end
      end

730 731
      context "where provides 'before' timestamp" do
        options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
Robert Speicher's avatar
Robert Speicher committed
732

733 734
        it "should returns commits on or before that timestamp" do
          commits = repository.log(options)
Robert Speicher's avatar
Robert Speicher committed
735

736 737 738 739
          expect(commits.size).to be > 0
          expect(commits).to satisfy do |commits|
            commits.all? { |commit| commit.committed_date <= options[:before] }
          end
Robert Speicher's avatar
Robert Speicher committed
740 741 742
        end
      end

743 744
      context 'when multiple paths are provided' do
        let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
Robert Speicher's avatar
Robert Speicher committed
745

746
        def commit_files(commit)
747
          Gitlab::GitalyClient::StorageSettings.allow_disk_access do
748 749
            commit.deltas.flat_map do |delta|
              [delta.old_path, delta.new_path].uniq.compact
750
            end
751
          end
752 753
        end

754 755
        it 'only returns commits matching at least one path' do
          commits = repository.log(options)
756

757 758 759 760
          expect(commits.size).to be > 0
          expect(commits).to satisfy do |commits|
            commits.none? { |commit| (commit_files(commit) & options[:path]).empty? }
          end
761 762 763
        end
      end

764 765 766 767
      context 'limit validation' do
        where(:limit) do
          [0, nil, '', 'foo']
        end
768

769 770
        with_them do
          it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) }
Robert Speicher's avatar
Robert Speicher committed
771 772
        end
      end
773

774 775 776
      context 'with all' do
        it 'returns a list of commits' do
          commits = repository.log({ all: true, limit: 50 })
777

778 779
          expect(commits.size).to eq(37)
        end
780 781
      end
    end
Tiago Botelho's avatar
Tiago Botelho committed
782

783 784 785
    context 'when Gitaly find_commits feature is enabled' do
      it_behaves_like 'repository log'
    end
Robert Speicher's avatar
Robert Speicher committed
786 787 788 789 790 791 792 793
  end

  describe '#count_commits_between' do
    subject { repository.count_commits_between('feature', 'master') }

    it { is_expected.to eq(17) }
  end

Rubén Dávila's avatar
Rubén Dávila committed
794
  describe '#raw_changes_between' do
795 796 797
    let(:old_rev) { }
    let(:new_rev) { }
    let(:changes) { repository.raw_changes_between(old_rev, new_rev) }
Rubén Dávila's avatar
Rubén Dávila committed
798

799 800 801
    context 'initial commit' do
      let(:old_rev) { Gitlab::Git::BLANK_SHA }
      let(:new_rev) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }
Rubén Dávila's avatar
Rubén Dávila committed
802

803 804 805
      it 'returns the changes' do
        expect(changes).to be_present
        expect(changes.size).to eq(3)
Rubén Dávila's avatar
Rubén Dávila committed
806
      end
807
    end
Rubén Dávila's avatar
Rubén Dávila committed
808

809 810 811
    context 'with an invalid rev' do
      let(:old_rev) { 'foo' }
      let(:new_rev) { 'bar' }
Rubén Dávila's avatar
Rubén Dávila committed
812

813 814
      it 'returns an error' do
        expect { changes }.to raise_error(Gitlab::Git::Repository::GitError)
Rubén Dávila's avatar
Rubén Dávila committed
815 816 817
      end
    end

818 819 820
    context 'with valid revs' do
      let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
      let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
Rubén Dávila's avatar
Rubén Dávila committed
821

822 823 824 825 826 827 828
      it 'returns the changes' do
        expect(changes.size).to eq(9)
        expect(changes.first.operation).to eq(:modified)
        expect(changes.first.new_path).to eq('.gitmodules')
        expect(changes.last.operation).to eq(:added)
        expect(changes.last.new_path).to eq('files/lfs/picture-invalid.png')
      end
Rubén Dávila's avatar
Rubén Dávila committed
829 830 831
    end
  end

832
  describe '#merge_base' do
833 834 835 836 837
    where(:from, :to, :result) do
      '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
      '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
      '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil
      'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil
838 839
    end

840 841
    with_them do
      it { expect(repository.merge_base(from, to)).to eq(result) }
842 843 844
    end
  end

845
  describe '#count_commits' do
846
    describe 'extended commit counting' do
847 848
      context 'with after timestamp' do
        it 'returns the number of commits after timestamp' do
849
          options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') }
850

851 852
          expect(repository.count_commits(options)).to eq(25)
        end
853 854
      end

855 856
      context 'with before timestamp' do
        it 'returns the number of commits before timestamp' do
857
          options = { ref: 'feature', before: Time.iso8601('2015-03-03T20:15:01+00:00') }
858

859 860
          expect(repository.count_commits(options)).to eq(9)
        end
861 862
      end

863 864 865 866 867 868 869 870
      context 'with max_count' do
        it 'returns the number of commits with path ' do
          options = { ref: 'master', max_count: 5 }

          expect(repository.count_commits(options)).to eq(5)
        end
      end

871 872
      context 'with path' do
        it 'returns the number of commits with path ' do
873 874 875 876 877 878 879 880 881
          options = { ref: 'master', path: 'encoding' }

          expect(repository.count_commits(options)).to eq(2)
        end
      end

      context 'with option :from and option :to' do
        it 'returns the number of commits ahead for fix-mode..fix-blob-path' do
          options = { from: 'fix-mode', to: 'fix-blob-path' }
882

883 884
          expect(repository.count_commits(options)).to eq(2)
        end
885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906

        it 'returns the number of commits ahead for fix-blob-path..fix-mode' do
          options = { from: 'fix-blob-path', to: 'fix-mode' }

          expect(repository.count_commits(options)).to eq(1)
        end

        context 'with option :left_right' do
          it 'returns the number of commits for fix-mode...fix-blob-path' do
            options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true }

            expect(repository.count_commits(options)).to eq([1, 2])
          end

          context 'with max_count' do
            it 'returns the number of commits with path ' do
              options = { from: 'fix-mode', to: 'fix-blob-path', left_right: true, max_count: 1 }

              expect(repository.count_commits(options)).to eq([1, 1])
            end
          end
        end
907
      end
908 909 910 911 912 913 914 915

      context 'with max_count' do
        it 'returns the number of commits up to the passed limit' do
          options = { ref: 'master', max_count: 10, after: Time.iso8601('2013-03-03T20:15:01+00:00') }

          expect(repository.count_commits(options)).to eq(10)
        end
      end
Tiago Botelho's avatar
Tiago Botelho committed
916 917 918 919 920 921 922 923 924 925 926

      context "with all" do
        it "returns the number of commits in the whole repository" do
          options = { all: true }

          expect(repository.count_commits(options)).to eq(34)
        end
      end

      context 'without all or ref being specified' do
        it "raises an ArgumentError" do
927
          expect { repository.count_commits({}) }.to raise_error(ArgumentError)
Tiago Botelho's avatar
Tiago Botelho committed
928 929
        end
      end
930
    end
931 932
  end

Robert Speicher's avatar
Robert Speicher committed
933
  describe '#find_branch' do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
934 935
    it 'should return a Branch for master' do
      branch = repository.find_branch('master')
Robert Speicher's avatar
Robert Speicher committed
936

Jacob Vosmaer's avatar
Jacob Vosmaer committed
937 938 939
      expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
      expect(branch.name).to eq('master')
    end
Robert Speicher's avatar
Robert Speicher committed
940

Jacob Vosmaer's avatar
Jacob Vosmaer committed
941 942
    it 'should handle non-existent branch' do
      branch = repository.find_branch('this-is-garbage')
Robert Speicher's avatar
Robert Speicher committed
943

Jacob Vosmaer's avatar
Jacob Vosmaer committed
944 945
      expect(branch).to eq(nil)
    end
946 947
  end

948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969
  describe '#ref_name_for_sha' do
    let(:ref_path) { 'refs/heads' }
    let(:sha) { repository.find_branch('master').dereferenced_target.id }
    let(:ref_name) { 'refs/heads/master' }

    it 'returns the ref name for the given sha' do
      expect(repository.ref_name_for_sha(ref_path, sha)).to eq(ref_name)
    end

    it "returns an empty name if the ref doesn't exist" do
      expect(repository.ref_name_for_sha(ref_path, "000000")).to eq("")
    end

    it "raise an exception if the ref is empty" do
      expect { repository.ref_name_for_sha(ref_path, "") }.to raise_error(ArgumentError)
    end

    it "raise an exception if the ref is nil" do
      expect { repository.ref_name_for_sha(ref_path, nil) }.to raise_error(ArgumentError)
    end
  end

970 971 972 973
  describe '#branches' do
    subject { repository.branches }

    context 'with local and remote branches' do
974
      let(:repository) { mutable_repository }
975 976

      before do
977
        create_remote_branch('joe', 'remote_branch', 'master')
978 979 980 981 982 983 984 985 986 987 988
        repository.create_branch('local_branch', 'master')
      end

      after do
        ensure_seeds
      end

      it 'returns the local and remote branches' do
        expect(subject.any? { |b| b.name == 'joe/remote_branch' }).to eq(true)
        expect(subject.any? { |b| b.name == 'local_branch' }).to eq(true)
      end
Robert Speicher's avatar
Robert Speicher committed
989
    end
990 991

    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branches
Robert Speicher's avatar
Robert Speicher committed
992 993 994 995
  end

  describe '#branch_count' do
    it 'returns the number of branches' do
996
      expect(repository.branch_count).to eq(11)
Robert Speicher's avatar
Robert Speicher committed
997
    end
998 999

    context 'with local and remote branches' do
1000
      let(:repository) { mutable_repository }
1001 1002

      before do
1003
        create_remote_branch('joe', 'remote_branch', 'master')
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024
        repository.create_branch('local_branch', 'master')
      end

      after do
        ensure_seeds
      end

      it 'returns the count of local branches' do
        expect(repository.branch_count).to eq(repository.local_branches.count)
      end

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

        it 'returns the count of local branches' do
          expect(repository.branch_count).to eq(repository.local_branches.count)
        end
      end
    end
Robert Speicher's avatar
Robert Speicher committed
1025 1026
  end

1027
  describe '#merged_branch_names' do
1028 1029 1030
    context 'when branch names are passed' do
      it 'only returns the names we are asking' do
        names = repository.merged_branch_names(%w[merge-test])
1031

1032 1033
        expect(names).to contain_exactly('merge-test')
      end
1034

1035 1036
      it 'does not return unmerged branch names' do
        names = repository.merged_branch_names(%w[feature])
1037

1038
        expect(names).to be_empty
1039
      end
1040
    end
1041

1042 1043 1044
    context 'when no root ref is available' do
      it 'returns empty list' do
        project = create(:project, :empty_repo)
1045

1046
        names = project.repository.merged_branch_names(%w[feature])
1047

1048
        expect(names).to be_empty
1049
      end
1050
    end
1051

1052 1053 1054 1055
    context 'when no branch names are specified' do
      before do
        repository.create_branch('identical', 'master')
      end
1056

1057 1058
      after do
        ensure_seeds
1059
      end
1060

1061 1062
      it 'returns all merged branch names except for identical one' do
        names = repository.merged_branch_names
1063

1064 1065 1066 1067 1068
        expect(names).to include('merge-test')
        expect(names).to include('fix-mode')
        expect(names).not_to include('feature')
        expect(names).not_to include('identical')
      end
1069
    end
1070 1071
  end

1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095
  describe '#diff_stats' do
    let(:left_commit_id) { 'feature' }
    let(:right_commit_id) { 'master' }

    it 'returns a DiffStatsCollection' do
      collection = repository.diff_stats(left_commit_id, right_commit_id)

      expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
      expect(collection).to be_a(Enumerable)
    end

    it 'yields Gitaly::DiffStats objects' do
      collection = repository.diff_stats(left_commit_id, right_commit_id)

      expect(collection.to_a).to all(be_a(Gitaly::DiffStats))
    end

    it 'returns no Gitaly::DiffStats when SHAs are invalid' do
      collection = repository.diff_stats('foo', 'bar')

      expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
      expect(collection).to be_a(Enumerable)
      expect(collection.to_a).to be_empty
    end
1096 1097 1098 1099 1100 1101 1102 1103

    it 'returns no Gitaly::DiffStats when there is a nil SHA' do
      collection = repository.diff_stats(nil, 'master')

      expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
      expect(collection).to be_a(Enumerable)
      expect(collection.to_a).to be_empty
    end
1104 1105
  end

Robert Speicher's avatar
Robert Speicher committed
1106 1107
  describe "#ls_files" do
    let(:master_file_paths) { repository.ls_files("master") }
1108
    let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
Robert Speicher's avatar
Robert Speicher committed
1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
    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

1119
    it "does not read submodule directory and empty directory of master branch" do
Robert Speicher's avatar
Robert Speicher committed
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129
      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
1130 1131 1132 1133

    it "returns valid utf-8 data" do
      expect(utf8_file_paths.map { |file| file.force_encoding('utf-8') }).to all(be_valid_encoding)
    end
Robert Speicher's avatar
Robert Speicher committed
1134 1135 1136
  end

  describe "#copy_gitattributes" do
1137
    let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
Robert Speicher's avatar
Robert Speicher committed
1138

1139 1140 1141
    after do
      FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
    end
Robert Speicher's avatar
Robert Speicher committed
1142

1143 1144 1145
    it "raises an error with invalid ref" do
      expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
    end
Robert Speicher's avatar
Robert Speicher committed
1146

1147 1148
    context 'when forcing encoding issues' do
      let(:branch_name) { "ʕ•ᴥ•ʔ" }
Robert Speicher's avatar
Robert Speicher committed
1149

1150 1151 1152
      before do
        repository.create_branch(branch_name, "master")
      end
Robert Speicher's avatar
Robert Speicher committed
1153

1154 1155
      after do
        repository.rm_branch(branch_name, user: build(:admin))
Robert Speicher's avatar
Robert Speicher committed
1156 1157
      end

1158 1159
      it "doesn't raise with a valid unicode ref" do
        expect { repository.copy_gitattributes(branch_name) }.not_to raise_error
Robert Speicher's avatar
Robert Speicher committed
1160

1161
        repository
Robert Speicher's avatar
Robert Speicher committed
1162
      end
1163
    end
Robert Speicher's avatar
Robert Speicher committed
1164

1165 1166 1167 1168
    context "with no .gitattrbutes" do
      before do
        repository.copy_gitattributes("master")
      end
Robert Speicher's avatar
Robert Speicher committed
1169

1170 1171 1172 1173
      it "does not have an info/attributes" do
        expect(File.exist?(attributes_path)).to be_falsey
      end
    end
Robert Speicher's avatar
Robert Speicher committed
1174

1175 1176 1177
    context "with .gitattrbutes" do
      before do
        repository.copy_gitattributes("gitattributes")
Robert Speicher's avatar
Robert Speicher committed
1178 1179
      end

1180 1181 1182
      it "has an info/attributes" do
        expect(File.exist?(attributes_path)).to be_truthy
      end
1183

1184 1185 1186 1187 1188
      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
    end
1189

1190 1191 1192 1193
    context "with updated .gitattrbutes" do
      before do
        repository.copy_gitattributes("gitattributes")
        repository.copy_gitattributes("gitattributes-updated")
Robert Speicher's avatar
Robert Speicher committed
1194 1195
      end

1196 1197 1198
      it "has an info/attributes" do
        expect(File.exist?(attributes_path)).to be_truthy
      end
1199

1200 1201 1202
      it "has the updated content in info/attributes" do
        contents = File.read(attributes_path)
        expect(contents).to eq("*.txt binary\n")
Robert Speicher's avatar
Robert Speicher committed
1203 1204
      end
    end
1205

1206 1207 1208 1209 1210
    context "with no .gitattrbutes in HEAD but with previous info/attributes" do
      before do
        repository.copy_gitattributes("gitattributes")
        repository.copy_gitattributes("master")
      end
1211

1212 1213 1214
      it "does not have an info/attributes" do
        expect(File.exist?(attributes_path)).to be_falsey
      end
1215
    end
Robert Speicher's avatar
Robert Speicher committed
1216 1217
  end

1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245
  describe '#gitattribute' do
    let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') }

    after do
      ensure_seeds
    end

    it 'returns matching language attribute' do
      expect(repository.gitattribute("custom-highlighting/test.gitlab-custom", 'gitlab-language')).to eq('ruby')
    end

    it 'returns matching language attribute with additional options' do
      expect(repository.gitattribute("custom-highlighting/test.gitlab-cgi", 'gitlab-language')).to eq('erb?parent=json')
    end

    it 'returns nil if nothing matches' do
      expect(repository.gitattribute("report.xslt", 'gitlab-language')).to eq(nil)
    end

    context 'without gitattributes file' do
      let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }

      it 'returns nil' do
        expect(repository.gitattribute("README.md", 'gitlab-language')).to eq(nil)
      end
    end
  end

1246
  describe '#ref_exists?' do
1247 1248 1249
    it 'returns true for an existing tag' do
      expect(repository.ref_exists?('refs/heads/master')).to eq(true)
    end
1250

1251 1252
    it 'returns false for a non-existing tag' do
      expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false)
1253 1254
    end

1255 1256
    it 'raises an ArgumentError for an empty string' do
      expect { repository.ref_exists?('') }.to raise_error(ArgumentError)
1257 1258
    end

1259 1260
    it 'raises an ArgumentError for an invalid ref' do
      expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError)
1261 1262 1263
    end
  end

Robert Speicher's avatar
Robert Speicher committed
1264
  describe '#tag_exists?' do
1265 1266
    it 'returns true for an existing tag' do
      tag = repository.tag_names.first
1267

1268
      expect(repository.tag_exists?(tag)).to eq(true)
1269 1270
    end

1271 1272
    it 'returns false for a non-existing tag' do
      expect(repository.tag_exists?('v9000')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1273 1274 1275 1276
    end
  end

  describe '#branch_exists?' do
1277 1278
    it 'returns true for an existing branch' do
      expect(repository.branch_exists?('master')).to eq(true)
Robert Speicher's avatar
Robert Speicher committed
1279 1280
    end

1281 1282
    it 'returns false for a non-existing branch' do
      expect(repository.branch_exists?('kittens')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1283 1284
    end

1285 1286
    it 'returns false when using an invalid branch name' do
      expect(repository.branch_exists?('.bla')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1287 1288 1289 1290
    end
  end

  describe '#local_branches' do
1291 1292 1293 1294 1295
    let(:repository) { mutable_repository }

    before do
      create_remote_branch('joe', 'remote_branch', 'master')
      repository.create_branch('local_branch', 'master')
Robert Speicher's avatar
Robert Speicher committed
1296 1297
    end

1298
    after do
Robert Speicher's avatar
Robert Speicher committed
1299 1300 1301 1302
      ensure_seeds
    end

    it 'returns the local branches' do
1303 1304
      expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false)
      expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true)
Robert Speicher's avatar
Robert Speicher committed
1305
    end
1306

1307
    it 'returns a Branch with UTF-8 fields' do
1308
      branches = repository.local_branches.to_a
1309 1310
      expect(branches.size).to be > 0
      branches.each do |branch|
1311 1312
        expect(branch.name).to be_utf8
        expect(branch.target).to be_utf8 unless branch.target.nil?
1313
      end
1314
    end