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
Jacob Vosmaer's avatar
Jacob Vosmaer committed
82
    it 'returns UTF-8' do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
83
      expect(repository.root_ref).to be_utf8
Jacob Vosmaer's avatar
Jacob Vosmaer committed
84 85
    end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
86
    it 'gets the branch name from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
87
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:default_branch_name)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
88 89
      repository.root_ref
    end
90

Andrew Newdigate's avatar
Andrew Newdigate committed
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
Jacob Vosmaer's avatar
Jacob Vosmaer committed
102 103

    it 'returns UTF-8' do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
104
      expect(subject.first).to be_utf8
Jacob Vosmaer's avatar
Jacob Vosmaer committed
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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
110
    it 'gets the branch names from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
111
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:branch_names)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
112 113
      subject
    end
114

Andrew Newdigate's avatar
Andrew Newdigate committed
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 }
Jacob Vosmaer's avatar
Jacob Vosmaer committed
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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
127
    it 'returns UTF-8' do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
128
      expect(subject.first).to be_utf8
Jacob Vosmaer's avatar
Jacob Vosmaer committed
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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
138
    it 'gets the tag names from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
139
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names)
Jacob Vosmaer's avatar
Jacob Vosmaer committed
140 141
      subject
    end
142

Andrew Newdigate's avatar
Andrew Newdigate committed
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