repository_spec.rb 71.5 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, '', 'group/project') }
23 24
  let(:mutable_repository_path) { File.join(TestEnv.repos_path, mutable_repository.relative_path) }
  let(:mutable_repository_rugged) { Rugged::Repository.new(mutable_repository_path) }
25
  let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
26 27
  let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
  let(:repository_rugged) { Rugged::Repository.new(repository_path) }
28
  let(:storage_path) { TestEnv.repos_path }
29
  let(:user) { build(:user) }
Robert Speicher's avatar
Robert Speicher committed
30

31
  describe '.create_hooks' do
32
    let(:repo_path) { File.join(storage_path, 'hook-test.git') }
33
    let(:hooks_dir) { File.join(repo_path, 'hooks') }
34
    let(:target_hooks_dir) { Gitlab::Shell.new.hooks_path }
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 74 75
    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
76 77 78 79 80 81 82
  describe "Respond to" do
    subject { repository }

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

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

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

Andrew Newdigate's avatar
Andrew Newdigate committed
93
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :default_branch_name do
94
      subject { repository.root_ref }
95
    end
96 97
  end

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

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

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

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

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

Andrew Newdigate's avatar
Andrew Newdigate committed
117
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branch_names
Robert Speicher's avatar
Robert Speicher committed
118 119
  end

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

    it { is_expected.to be_kind_of Array }
124

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

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

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

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

Andrew Newdigate's avatar
Andrew Newdigate committed
145
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :tag_names
Robert Speicher's avatar
Robert Speicher committed
146 147
  end

148 149 150
  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
151

152 153 154
    let(:append_sha) { true }
    let(:ref) { 'master' }
    let(:format) { nil }
155
    let(:path) { nil }
156

157 158 159 160
    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}" }
161

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

164 165
    it 'sets CommitId to the commit SHA' do
      expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
166
    end
167

168 169
    it 'sets ArchivePrefix to the expected prefix' do
      expect(metadata['ArchivePrefix']).to eq(expected_prefix)
170
    end
171

172 173 174 175
    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
176

177 178
      expect(metadata['ArchivePath']).to eq(expected_path)
    end
Robert Speicher's avatar
Robert Speicher committed
179

180 181 182 183 184 185 186 187
    context 'path is set' do
      let(:path) { 'foo/bar' }

      it 'appends the path to the prefix' do
        expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar")
      end
    end

188 189 190
    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
191

192 193 194 195 196 197 198
        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
199

200 201 202 203 204
      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
205

206 207 208 209 210 211 212
    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
213

214 215 216 217 218
      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
219 220
  end

221
  describe '#size' do
Robert Speicher's avatar
Robert Speicher committed
222 223 224 225 226
    subject { repository.size }

    it { is_expected.to be < 2 }
  end

227
  describe '#empty?' do
228
    it { expect(repository).not_to be_empty }
Robert Speicher's avatar
Robert Speicher committed
229 230
  end

231
  describe '#ref_names' do
Robert Speicher's avatar
Robert Speicher committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
    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

248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271
  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

272 273 274 275 276 277 278
    context 'no .gitmodules at commit' do
      let(:ref) { '9596bc54a6f0c0c98248fe97077eb5ccf48a98d0' }

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

    context 'no gitlink entry' do
279 280 281 282 283 284
      let(:ref) { '6d39438' }

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

285
  describe '#commit_count' do
286 287 288
    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) }
289

290 291
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do
      subject { repository.commit_count('master') }
292
    end
Robert Speicher's avatar
Robert Speicher committed
293 294
  end

295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
  describe '#diverging_commit_count' do
    it 'counts 0 for the same branch' do
      expect(repository.diverging_commit_count('master', 'master', max_count: 1000)).to eq([0, 0])
    end

    context 'max count does not truncate results' do
      where(:left, :right, :expected) do
        1 | 1 | [1, 1]
        4 | 4 | [4, 4]
        2 | 2 | [2, 2]
        2 | 4 | [2, 4]
        4 | 2 | [4, 2]
        10 | 10 | [10, 10]
      end

      with_them do
        before do
          repository.create_branch('left-branch', 'master')
          repository.create_branch('right-branch', 'master')
314

315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
          left.times do
            new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff')
          end

          right.times do
            new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff')
          end
        end

        after do
          repository.delete_branch('left-branch')
          repository.delete_branch('right-branch')
        end

        it 'returns the correct count bounding at max_count' do
          branch_a_sha = repository_rugged.branches['left-branch'].target.oid
          branch_b_sha = repository_rugged.branches['right-branch'].target.oid
332 333 334 335

          count = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: 1000)

          expect(count).to eq(expected)
336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353
        end
      end
    end

    context 'max count truncates results' do
      where(:left, :right, :max_count) do
        1 | 1 | 1
        4 | 4 | 4
        2 | 2 | 3
        2 | 4 | 3
        4 | 2 | 5
        10 | 10 | 10
      end

      with_them do
        before do
          repository.create_branch('left-branch', 'master')
          repository.create_branch('right-branch', 'master')
354

355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
          left.times do
            new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'left-branch', 'some more content for a', 'some stuff')
          end

          right.times do
            new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'right-branch', 'some more content for b', 'some stuff')
          end
        end

        after do
          repository.delete_branch('left-branch')
          repository.delete_branch('right-branch')
        end

        it 'returns the correct count bounding at max_count' do
          branch_a_sha = repository_rugged.branches['left-branch'].target.oid
          branch_b_sha = repository_rugged.branches['right-branch'].target.oid
372

373
          results = repository.diverging_commit_count(branch_a_sha, branch_b_sha, max_count: max_count)
374

375 376 377 378 379 380 381 382 383 384
          expect(results[0] + results[1]).to eq(max_count)
        end
      end
    end

    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :diverging_commit_count do
      subject { repository.diverging_commit_count('master', 'master', max_count: 1000) }
    end
  end

385
  describe '#has_local_branches?' do
386
    context 'check for local branches' do
387 388 389
      it { expect(repository.has_local_branches?).to eq(true) }

      context 'mutable' do
390
        let(:repository) { mutable_repository }
391 392 393 394 395 396 397

        after do
          ensure_seeds
        end

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

400 401
          FileUtils.rm_rf(File.join(repository_path, 'packed-refs'))
          heads_dir = File.join(repository_path, 'refs/heads')
402 403 404
          FileUtils.rm_rf(heads_dir)
          FileUtils.mkdir_p(heads_dir)

405
          repository.expire_has_local_branches_cache
406 407 408
          expect(repository.has_local_branches?).to eq(false)
        end
      end
409 410 411 412 413 414 415 416 417 418

      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
419 420 421
    end
  end

Robert Speicher's avatar
Robert Speicher committed
422
  describe "#delete_branch" do
423
    let(:repository) { mutable_repository }
424

425 426 427
    after do
      ensure_seeds
    end
428

429 430
    it "removes the branch from the repo" do
      branch_name = "to-be-deleted-soon"
431

432 433
      repository.create_branch(branch_name)
      expect(repository_rugged.branches[branch_name]).not_to be_nil
Robert Speicher's avatar
Robert Speicher committed
434

435 436
      repository.delete_branch(branch_name)
      expect(repository_rugged.branches[branch_name]).to be_nil
Robert Speicher's avatar
Robert Speicher committed
437 438
    end

439 440 441 442
    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
443 444 445 446
    end
  end

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

449 450 451
    after do
      ensure_seeds
    end
452

453
    it "creates a new branch" do
454 455
      expect(repository.create_branch('new_branch', 'master')).not_to be_nil
    end
456

457
    it "creates a new branch with the right name" do
458
      expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch')
Robert Speicher's avatar
Robert Speicher committed
459 460
    end

461
    it "fails if we create an existing branch" do
462 463
      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
464 465
    end

466
    it "fails if we create a branch from a non existing ref" do
467
      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
468 469 470
    end
  end

471
  describe '#delete_refs' do
472
    let(:repository) { mutable_repository }
473

474 475 476
    after do
      ensure_seeds
    end
477

478
    it 'deletes the ref' do
479
      repository.delete_refs('refs/heads/feature')
480

481
      expect(repository_rugged.references['refs/heads/feature']).to be_nil
482
    end
483

484 485
    it 'deletes all refs' do
      refs = %w[refs/heads/wip refs/tags/v1.1.0]
486
      repository.delete_refs(*refs)
487

488
      refs.each do |ref|
489
        expect(repository_rugged.references[ref]).to be_nil
490 491 492
      end
    end

493
    it 'does not fail when deleting an empty list of refs' do
494
      expect { repository.delete_refs(*[]) }.not_to raise_error
495 496
    end

497
    it 'raises an error if it failed' do
498
      expect { repository.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError)
499 500 501
    end
  end

502
  describe '#branch_names_contains_sha' do
503
    let(:head_id) { repository_rugged.head.target.oid }
504 505
    let(:new_branch) { head_id }
    let(:utf8_branch) { 'branch-é' }
506

507 508 509
    before do
      repository.create_branch(new_branch, 'master')
      repository.create_branch(utf8_branch, 'master')
510 511
    end

512 513 514
    after do
      repository.delete_branch(new_branch)
      repository.delete_branch(utf8_branch)
515 516
    end

517 518
    it 'displays that branch' do
      expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch, utf8_branch)
519 520 521
    end
  end

Robert Speicher's avatar
Robert Speicher committed
522
  describe "#refs_hash" do
523
    subject { repository.refs_hash }
Robert Speicher's avatar
Robert Speicher committed
524

525
    it "has as many entries as branches and tags" do
Robert Speicher's avatar
Robert Speicher committed
526 527
      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
528 529 530 531 532
      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
533
    end
534 535 536 537 538 539 540

    it 'does not error when dereferenced_target is nil' do
      blob_id = repository.blob_at('master', 'README.md').id
      repository_rugged.tags.create("refs/tags/blob-tag", blob_id)

      expect { subject }.not_to raise_error
    end
Robert Speicher's avatar
Robert Speicher committed
541 542
  end

543
  describe '#fetch_repository_as_mirror' do
544
    let(:new_repository) do
545
      Gitlab::Git::Repository.new('default', 'my_project.git', '', 'group/project')
Robert Speicher's avatar
Robert Speicher committed
546 547
    end

548
    subject { new_repository.fetch_repository_as_mirror(repository) }
549 550

    before do
551
      Gitlab::Shell.new.create_repository('default', 'my_project', 'group/project')
Robert Speicher's avatar
Robert Speicher committed
552 553
    end

554
    after do
555
      Gitlab::Shell.new.remove_repository('default', 'my_project')
556 557
    end

558 559
    it 'fetches a repository as a mirror remote' do
      subject
560

561 562
      expect(refs(new_repository_path)).to eq(refs(repository_path))
    end
563

564 565 566 567
    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}" }
568

569 570 571
      before do
        repository_rugged.references.create(keep_around_ref, sha, force: true)
        repository_rugged.references.create(tmp_ref, sha, force: true)
572
      end
573

574 575
      it 'includes the temporary and keep-around refs' do
        subject
576

577 578 579
        expect(refs(new_repository_path)).to include(keep_around_ref)
        expect(refs(new_repository_path)).to include(tmp_ref)
      end
580
    end
581 582

    def new_repository_path
583
      File.join(TestEnv.repos_path, new_repository.relative_path)
584
    end
585 586
  end

587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607
  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

608 609 610 611 612 613 614 615 616 617 618 619 620 621 622
  describe '#search_files_by_content' do
    let(:repository) { mutable_repository }
    let(:repository_rugged) { mutable_repository_rugged }

    before do
      repository.create_branch('search-files-by-content-branch', 'master')
      new_commit_edit_new_file_on_branch(repository_rugged, 'encoding/CHANGELOG', 'search-files-by-content-branch', 'committing something', 'search-files-by-content change')
      new_commit_edit_new_file_on_branch(repository_rugged, 'anotherfile', 'search-files-by-content-branch', 'committing something', 'search-files-by-content change')
    end

    after do
      ensure_seeds
    end

    shared_examples 'search files by content' do
623
      it 'has 2 items' do
624 625 626
        expect(search_results.size).to eq(2)
      end

627
      it 'has the correct matching line' do
628 629 630 631 632 633 634 635 636 637 638 639
        expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n",
                                                  "search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n")
      end
    end

    it_should_behave_like 'search files by content' do
      let(:search_results) do
        repository.search_files_by_content('search-files-by-content', 'search-files-by-content-branch')
      end
    end
  end

640 641 642 643 644 645 646 647
  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

648 649 650 651
    it 'returns UTF-8' do
      expect(repository.find_remote_root_ref('origin')).to be_utf8
    end

652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
    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
671
  describe "#log" do
672 673
    shared_examples 'repository log' do
      let(:commit_with_old_name) do
674
        Gitlab::Git::Commit.find(repository, @commit_with_old_name_id)
675 676
      end
      let(:commit_with_new_name) do
677
        Gitlab::Git::Commit.find(repository, @commit_with_new_name_id)
678 679
      end
      let(:rename_commit) do
680
        Gitlab::Git::Commit.find(repository, @rename_commit_id)
681
      end
Robert Speicher's avatar
Robert Speicher committed
682

683
      before do
684
        # Add new commits so that there's a renamed file in the commit history
685 686
        @commit_with_old_name_id = new_commit_edit_old_file(repository_rugged).oid
        @rename_commit_id = new_commit_move_file(repository_rugged).oid
687
        @commit_with_new_name_id = new_commit_edit_new_file(repository_rugged, "encoding/CHANGELOG", "Edit encoding/CHANGELOG", "I'm a new changelog with different text").oid
688
      end
Robert Speicher's avatar
Robert Speicher committed
689

690
      after do
691
        # Erase our commits so other tests get the original repo
692
        repository_rugged.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
Robert Speicher's avatar
Robert Speicher committed
693 694
      end

695 696 697 698 699 700
      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"))
701 702 703 704

            aggregate_failures do
              expect(log_commits).to include(commit_with_new_name)
              expect(log_commits).to include(rename_commit)
705
              expect(log_commits).not_to include(commit_with_old_name)
706
            end
707
          end
Robert Speicher's avatar
Robert Speicher committed
708 709
        end

710 711 712 713
        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"))
714

715 716 717 718 719
              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
720
            end
721 722
          end

723 724 725
          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))
726

727 728 729 730 731 732
              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
733 734
          end

735 736 737
          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))
738

739
              expect(log_commits).to contain_exactly(rename_commit)
740
            end
741 742
          end

743 744 745
          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))
746

747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769
              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)
770
            end
771 772
          end

773 774 775
          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))
776

777 778 779 780 781 782
              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
783 784 785
          end
        end

786 787 788
        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"))
789 790 791

            aggregate_failures do
              expect(log_commits).not_to include(commit_with_new_name)
792
              expect(log_commits).to include(rename_commit)
793 794
              expect(log_commits).to include(commit_with_old_name)
            end
795
          end
Robert Speicher's avatar
Robert Speicher committed
796 797
        end

798 799 800
        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
801

802
            expect(log_commits).to eq([])
803
          end
Robert Speicher's avatar
Robert Speicher committed
804 805 806
        end
      end

807 808
      context "where 'follow' == false" do
        options = { follow: false }
Robert Speicher's avatar
Robert Speicher committed
809

810 811 812 813
        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
814

815 816 817 818 819
          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
820 821
        end

822 823 824 825
        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
826

827 828 829 830 831
          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
832 833
        end

834 835 836 837
        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
838

839 840 841 842 843
          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
844 845
        end

846 847 848 849 850 851 852 853
        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
854 855 856
        end
      end

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

860
        it "returns commits on or after that timestamp" do
861 862 863 864 865 866
          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
867 868 869
        end
      end

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

873
        it "returns commits on or before that timestamp" do
874
          commits = repository.log(options)
Robert Speicher's avatar
Robert Speicher committed
875

876 877 878 879
          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
880 881 882
        end
      end

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

886
        def commit_files(commit)
887
          Gitlab::GitalyClient::StorageSettings.allow_disk_access do
888 889
            commit.deltas.flat_map do |delta|
              [delta.old_path, delta.new_path].uniq.compact
890
            end
891
          end
892 893
        end

894 895
        it 'only returns commits matching at least one path' do
          commits = repository.log(options)
896

897 898 899 900
          expect(commits.size).to be > 0
          expect(commits).to satisfy do |commits|
            commits.none? { |commit| (commit_files(commit) & options[:path]).empty? }
          end
901 902 903
        end
      end

904 905 906 907
      context 'limit validation' do
        where(:limit) do
          [0, nil, '', 'foo']
        end
908

909 910
        with_them do
          it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) }
Robert Speicher's avatar
Robert Speicher committed
911 912
        end
      end
913

914 915 916
      context 'with all' do
        it 'returns a list of commits' do
          commits = repository.log({ all: true, limit: 50 })
917

918 919
          expect(commits.size).to eq(37)
        end
920 921
      end
    end
Tiago Botelho's avatar
Tiago Botelho committed
922

923 924 925
    context 'when Gitaly find_commits feature is enabled' do
      it_behaves_like 'repository log'
    end
Robert Speicher's avatar
Robert Speicher committed
926 927 928 929 930 931 932 933
  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
934
  describe '#raw_changes_between' do
935 936 937
    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
938

939 940 941
    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
942

943 944 945
      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
946
      end
947
    end
Rubén Dávila's avatar
Rubén Dávila committed
948

949 950 951
    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
952

953 954
      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
955 956 957
      end
    end

958 959 960
    context 'with valid revs' do
      let(:old_rev) { 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6' }
      let(:new_rev) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
Rubén Dávila's avatar
Rubén Dávila committed
961

962 963 964 965 966 967 968
      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
969 970 971
    end
  end

972
  describe '#merge_base' do
973 974 975 976 977
    where(:from, :to, :result) do
      '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
      '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
      '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil
      'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil
978 979
    end

980 981
    with_them do
      it { expect(repository.merge_base(from, to)).to eq(result) }
982 983 984
    end
  end

985
  describe '#count_commits' do
986
    describe 'extended commit counting' do
987 988
      context 'with after timestamp' do
        it 'returns the number of commits after timestamp' do
989
          options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') }
990

991 992
          expect(repository.count_commits(options)).to eq(25)
        end
993 994
      end

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

999 1000
          expect(repository.count_commits(options)).to eq(9)
        end
1001 1002
      end

1003 1004 1005 1006 1007 1008 1009 1010
      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

1011 1012
      context 'with path' do
        it 'returns the number of commits with path ' do
1013 1014 1015 1016 1017 1018 1019 1020 1021
          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' }
1022

1023 1024
          expect(repository.count_commits(options)).to eq(2)
        end
1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046

        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
1047
      end
1048 1049 1050 1051 1052 1053 1054 1055

      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
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066

      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
1067
          expect { repository.count_commits({}) }.to raise_error(ArgumentError)
Tiago Botelho's avatar
Tiago Botelho committed
1068 1069
        end
      end
1070
    end
1071 1072
  end

Robert Speicher's avatar
Robert Speicher committed
1073
  describe '#find_branch' do
1074
    it 'returns a Branch for master' do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
1075
      branch = repository.find_branch('master')
Robert Speicher's avatar
Robert Speicher committed
1076

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1077 1078 1079
      expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
      expect(branch.name).to eq('master')
    end
Robert Speicher's avatar
Robert Speicher committed
1080

1081
    it 'handles non-existent branch' do
Jacob Vosmaer's avatar
Jacob Vosmaer committed
1082
      branch = repository.find_branch('this-is-garbage')
Robert Speicher's avatar
Robert Speicher committed
1083

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1084 1085
      expect(branch).to eq(nil)
    end
1086 1087
  end

1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109
  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

1110 1111 1112 1113
  describe '#branches' do
    subject { repository.branches }

    context 'with local and remote branches' do
1114
      let(:repository) { mutable_repository }
1115 1116

      before do
1117
        create_remote_branch('joe', 'remote_branch', 'master')
1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128
        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
1129
    end
1130 1131

    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :branches
Robert Speicher's avatar
Robert Speicher committed
1132 1133 1134 1135
  end

  describe '#branch_count' do
    it 'returns the number of branches' do
1136
      expect(repository.branch_count).to eq(11)
Robert Speicher's avatar
Robert Speicher committed
1137
    end
1138 1139

    context 'with local and remote branches' do
1140
      let(:repository) { mutable_repository }
1141 1142

      before do
1143
        create_remote_branch('joe', 'remote_branch', 'master')
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164
        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
1165 1166
  end

1167
  describe '#merged_branch_names' do
1168 1169 1170
    context 'when branch names are passed' do
      it 'only returns the names we are asking' do
        names = repository.merged_branch_names(%w[merge-test])
1171

1172 1173
        expect(names).to contain_exactly('merge-test')
      end
1174

1175 1176
      it 'does not return unmerged branch names' do
        names = repository.merged_branch_names(%w[feature])
1177

1178
        expect(names).to be_empty
1179
      end
1180
    end
1181

1182 1183 1184
    context 'when no root ref is available' do
      it 'returns empty list' do
        project = create(:project, :empty_repo)
1185

1186
        names = project.repository.merged_branch_names(%w[feature])
1187

1188
        expect(names).to be_empty
1189
      end
1190
    end
1191

1192 1193 1194 1195
    context 'when no branch names are specified' do
      before do
        repository.create_branch('identical', 'master')
      end
1196

1197 1198
      after do
        ensure_seeds
1199
      end
1200

1201 1202
      it 'returns all merged branch names except for identical one' do
        names = repository.merged_branch_names
1203

1204 1205 1206 1207 1208
        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
1209
    end
1210 1211
  end

1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235
  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
1236 1237

    it 'returns no Gitaly::DiffStats when there is a nil SHA' do
1238 1239 1240
      expect_any_instance_of(Gitlab::GitalyClient::CommitService)
        .not_to receive(:diff_stats)

1241 1242 1243 1244 1245 1246
      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
1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257

    it 'returns no Gitaly::DiffStats when there is a BLANK_SHA' do
      expect_any_instance_of(Gitlab::GitalyClient::CommitService)
        .not_to receive(:diff_stats)

      collection = repository.diff_stats(Gitlab::Git::BLANK_SHA, 'master')

      expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
      expect(collection).to be_a(Enumerable)
      expect(collection.to_a).to be_empty
    end
1258 1259
  end

Robert Speicher's avatar
Robert Speicher committed
1260 1261
  describe "#ls_files" do
    let(:master_file_paths) { repository.ls_files("master") }
1262
    let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
Robert Speicher's avatar
Robert Speicher committed
1263 1264 1265 1266 1267 1268 1269 1270 1271 1272
    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

1273
    it "does not read submodule directory and empty directory of master branch" do
Robert Speicher's avatar
Robert Speicher committed
1274 1275 1276 1277 1278 1279 1280 1281 1282 1283
      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
1284 1285 1286 1287

    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
1288 1289 1290
  end

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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1293 1294 1295
    after do
      FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
    end
Robert Speicher's avatar
Robert Speicher committed
1296

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1297 1298 1299
    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
1300

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1301 1302
    context 'when forcing encoding issues' do
      let(:branch_name) { "ʕ•ᴥ•ʔ" }
Robert Speicher's avatar
Robert Speicher committed
1303

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1304 1305 1306
      before do
        repository.create_branch(branch_name, "master")
      end
Robert Speicher's avatar
Robert Speicher committed
1307

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1308 1309
      after do
        repository.rm_branch(branch_name, user: build(:admin))
Robert Speicher's avatar
Robert Speicher committed
1310 1311
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1312 1313
      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
1314

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1315
        repository
Robert Speicher's avatar
Robert Speicher committed
1316
      end
Jacob Vosmaer's avatar
Jacob Vosmaer committed
1317
    end
Robert Speicher's avatar
Robert Speicher committed
1318

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1319 1320 1321 1322
    context "with no .gitattrbutes" do
      before do
        repository.copy_gitattributes("master")
      end
Robert Speicher's avatar
Robert Speicher committed
1323

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1324 1325 1326 1327
      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
1328

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1329 1330 1331
    context "with .gitattrbutes" do
      before do
        repository.copy_gitattributes("gitattributes")
Robert Speicher's avatar
Robert Speicher committed
1332 1333
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1334 1335 1336
      it "has an info/attributes" do
        expect(File.exist?(attributes_path)).to be_truthy
      end
1337

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1338 1339 1340 1341 1342
      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
1343

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1344 1345 1346 1347
    context "with updated .gitattrbutes" do
      before do
        repository.copy_gitattributes("gitattributes")
        repository.copy_gitattributes("gitattributes-updated")
Robert Speicher's avatar
Robert Speicher committed
1348 1349
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1350 1351 1352
      it "has an info/attributes" do
        expect(File.exist?(attributes_path)).to be_truthy
      end
1353

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1354 1355 1356
      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
1357 1358
      end
    end
1359

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1360 1361 1362 1363 1364
    context "with no .gitattrbutes in HEAD but with previous info/attributes" do
      before do
        repository.copy_gitattributes("gitattributes")
        repository.copy_gitattributes("master")
      end
1365

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1366 1367 1368
      it "does not have an info/attributes" do
        expect(File.exist?(attributes_path)).to be_falsey
      end
1369
    end
Robert Speicher's avatar
Robert Speicher committed
1370 1371
  end

1372
  describe '#gitattribute' do
1373
    let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') }
1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391

    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
1392
      let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
1393 1394 1395 1396 1397 1398 1399

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

1400
  describe '#ref_exists?' do
1401 1402 1403
    it 'returns true for an existing tag' do
      expect(repository.ref_exists?('refs/heads/master')).to eq(true)
    end
1404

1405 1406
    it 'returns false for a non-existing tag' do
      expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false)
1407 1408
    end

1409 1410
    it 'raises an ArgumentError for an empty string' do
      expect { repository.ref_exists?('') }.to raise_error(ArgumentError)
1411 1412
    end

1413 1414
    it 'raises an ArgumentError for an invalid ref' do
      expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError)
1415 1416 1417
    end
  end

Robert Speicher's avatar
Robert Speicher committed
1418
  describe '#tag_exists?' do
1419 1420
    it 'returns true for an existing tag' do
      tag = repository.tag_names.first
1421

1422
      expect(repository.tag_exists?(tag)).to eq(true)
1423 1424
    end

1425 1426
    it 'returns false for a non-existing tag' do
      expect(repository.tag_exists?('v9000')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1427 1428 1429 1430
    end
  end

  describe '#branch_exists?' do
1431 1432
    it 'returns true for an existing branch' do
      expect(repository.branch_exists?('master')).to eq(true)
Robert Speicher's avatar
Robert Speicher committed
1433 1434
    end

1435 1436
    it 'returns false for a non-existing branch' do
      expect(repository.branch_exists?('kittens')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1437 1438
    end

1439 1440
    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
1441 1442 1443 1444
    end
  end

  describe '#local_branches' do
1445 1446 1447 1448 1449
    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
1450 1451
    end

1452
    after do
Robert Speicher's avatar
Robert Speicher committed
1453 1454 1455 1456
      ensure_seeds
    end

    it 'returns the local branches' do
1457 1458
      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
1459
    end
1460

1461
    it 'returns a Branch with UTF-8 fields' do
1462
      branches = repository.local_branches.to_a
1463 1464
      expect(branches.size).to be > 0
      branches.each do |branch|
1465 1466
        expect(branch.name).to be_utf8
        expect(branch.target).to be_utf8 unless branch.target.nil?
1467
      end
1468
    end
1469

1470
    it 'gets the branches from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
1471
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:local_branches)
1472
        .and_return([])
1473
      repository.local_branches
1474
    end
1475

Andrew Newdigate's avatar
Andrew Newdigate committed
1476
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :local_branches do
1477
      subject { repository.local_branches }
1478
    end
Robert Speicher's avatar
Robert Speicher committed
1479 1480
  end

1481
  describe '#languages' do
1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495
    it 'returns exactly the expected results' do
      languages = repository.languages('4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6')
      expected_languages = [
        { value: 66.63, label: "Ruby", color: "#701516", highlight: "#701516" },
        { value: 22.96, label: "JavaScript", color: "#f1e05a", highlight: "#f1e05a" },
        { value: 7.9, label: "HTML", color: "#e34c26", highlight: "#e34c26" },
        { value: 2.51, label: "CoffeeScript", color: "#244776", highlight: "#244776" }
      ]

      expect(languages.size).to eq(expected_languages.size)

      expected_languages.size.times do |i|
        a = expected_languages[i]
        b = languages[i]
1496

1497 1498
        expect(a.keys.sort).to eq(b.keys.sort)
        expect(a[:value]).to be_within(0.1).of(b[:value])
1499

1500 1501
        non_float_keys = a.keys - [:value]
        expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys))
1502 1503 1504
      end
    end

1505 1506
    it "uses the repository's HEAD when no ref is passed" do
      lang = repository.languages.first
1507

1508
      expect(lang[:label]).to eq('Ruby')
1509 1510 1511
    end
  end

1512
  describe '#license_short_name' do
1513
    subject { repository.license_short_name }
1514

1515 1516 1517
    context 'when no license file can be found' do
      let(:project) { create(:project, :repository) }
      let(:repository) { project.repository.raw_repository }
1518

1519 1520
      before do
        project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
1521 1522
      end

1523
      it { is_expected.to be_nil }
1524 1525
    end

1526 1527
    context 'when an mit license is found' do
      it { is_expected.to eq('mit') }
1528 1529 1530
    end
  end

1531
  describe '#fetch_source_branch!' do
1532
    let(:local_ref) { 'refs/merge-requests/1/head' }
1533
    let(:source_repository) { mutable_repository }
1534

1535 1536 1537
    after do
      ensure_seeds
    end
1538

1539 1540 1541
    context 'when the branch exists' do
      context 'when the commit does not exist locally' do
        let(:source_branch) { 'new-branch-for-fetch-source-branch' }
1542 1543
        let(:source_path) { File.join(TestEnv.repos_path, source_repository.relative_path) }
        let(:source_rugged) { Rugged::Repository.new(source_path) }
1544
        let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
1545

1546 1547
        before do
          source_rugged.branches.create(source_branch, new_oid)
1548
        end
1549

1550 1551 1552
        it 'writes the ref' do
          expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
          expect(repository.commit(local_ref).sha).to eq(new_oid)
1553
        end
1554 1555
      end

1556 1557 1558 1559 1560 1561 1562
      context 'when the commit exists locally' do
        let(:source_branch) { 'master' }
        let(:expected_oid) { SeedRepo::LastCommit::ID }

        it 'writes the ref' do
          # Sanity check: the commit should already exist
          expect(repository.commit(expected_oid)).not_to be_nil
1563

1564 1565
          expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
          expect(repository.commit(local_ref).sha).to eq(expected_oid)
1566
        end
1567 1568
      end
    end
1569

1570 1571
    context 'when the branch does not exist' do
      let(:source_branch) { 'definitely-not-master' }
1572

1573 1574 1575 1576
      it 'does not write the ref' do
        expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(false)
        expect(repository.commit(local_ref)).to be_nil
      end
1577
    end
1578 1579
  end

1580
  describe '#rm_branch' do
1581 1582 1583
    let(:project) { create(:project, :repository) }
    let(:repository) { project.repository.raw }
    let(:branch_name) { "to-be-deleted-soon" }
1584

1585 1586 1587
    before do
      project.add_developer(user)
      repository.create_branch(branch_name)
1588 1589
    end

1590 1591
    it "removes the branch from the repo" do
      repository.rm_branch(branch_name, user: user)
1592

1593
      expect(repository_rugged.branches[branch_name]).to be_nil
1594 1595 1596
    end
  end

1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611
  describe '#write_ref' do
    context 'validations' do
      using RSpec::Parameterized::TableSyntax

      where(:ref_path, :ref) do
        'foo bar' | '123'
        'foobar'  | "12\x003"
      end

      with_them do
        it 'raises ArgumentError' do
          expect { repository.write_ref(ref_path, ref) }.to raise_error(ArgumentError)
        end
      end
    end
1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624

    it 'writes the HEAD' do
      repository.write_ref('HEAD', 'refs/heads/feature')

      expect(repository.commit('HEAD')).to eq(repository.commit('feature'))
      expect(repository.root_ref).to eq('feature')
    end

    it 'writes other refs' do
      repository.write_ref('refs/heads/feature', SeedRepo::Commit::ID)

      expect(repository.commit('feature').sha).to eq(SeedRepo::Commit::ID)
    end
1625 1626
  end

1627 1628
  describe '#write_config' do
    before do
1629
      repository_rugged.config["gitlab.fullpath"] = repository_path
1630 1631
    end

1632 1633 1634
    context 'is given a path' do
      it 'writes it to disk' do
        repository.write_config(full_path: "not-the/real-path.git")
1635

1636
        config = File.read(File.join(repository_path, "config"))
1637

1638 1639
        expect(config).to include("[gitlab]")
        expect(config).to include("fullpath = not-the/real-path.git")
1640
      end
1641
    end
1642

1643 1644 1645
    context 'it is given an empty path' do
      it 'does not write it to disk' do
        repository.write_config(full_path: "")
1646

1647
        config = File.read(File.join(repository_path, "config"))
1648

1649 1650
        expect(config).to include("[gitlab]")
        expect(config).to include("fullpath = #{repository_path}")
1651
      end
1652
    end
1653

1654 1655
    context 'repository does not exist' do
      it 'raises NoRepository and does not call Gitaly WriteConfig' do
1656
        repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '', 'group/project')
1657

1658
        expect(repository.gitaly_repository_client).not_to receive(:write_config)
1659

1660 1661 1662
        expect do
          repository.write_config(full_path: 'foo/bar.git')
        end.to raise_error(Gitlab::Git::Repository::NoRepository)
1663
      end
1664 1665 1666
    end
  end

1667
  describe '#set_config' do
1668
    let(:repository) { mutable_repository }
1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679
    let(:entries) do
      {
        'test.foo1' => 'bla bla',
        'test.foo2' => 1234,
        'test.foo3' => true
      }
    end

    it 'can set config settings' do
      expect(repository.set_config(entries)).to be_nil

1680 1681 1682
      expect(repository_rugged.config['test.foo1']).to eq('bla bla')
      expect(repository_rugged.config['test.foo2']).to eq('1234')
      expect(repository_rugged.config['test.foo3']).to eq('true')
1683 1684 1685
    end

    after do
1686
      entries.keys.each { |k| repository_rugged.config.delete(k) }
1687 1688 1689 1690
    end
  end

  describe '#delete_config' do
1691
    let(:repository) { mutable_repository }
1692 1693 1694 1695 1696 1697 1698 1699 1700 1701
    let(:entries) do
      {
        'test.foo1' => 'bla bla',
        'test.foo2' => 1234,
        'test.foo3' => true
      }
    end

    it 'can delete config settings' do
      entries.each do |key, value|
1702
        repository_rugged.config[key] = value
1703 1704 1705 1706
      end

      expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil

Stan Hu's avatar
Stan Hu committed
1707 1708 1709 1710 1711
      # Workaround for https://github.com/libgit2/rugged/issues/785: If
      # Gitaly changes .gitconfig while Rugged has the file loaded
      # Rugged::Repository#each_key will report stale values unless a
      # lookup is done first.
      expect(repository_rugged.config['test.foo1']).to be_nil
1712
      config_keys = repository_rugged.config.each_key.to_a
1713 1714 1715 1716 1717
      expect(config_keys).not_to include('test.foo1')
      expect(config_keys).not_to include('test.foo2')
    end
  end

1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748
  describe '#merge_to_ref' do
    let(:repository) { mutable_repository }
    let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
    let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
    let(:right_branch) { 'test-master' }
    let(:target_ref) { 'refs/merge-requests/999/merge' }

    before do
      repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch)
    end

    def merge_to_ref
      repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message')
    end

    it 'generates a commit in the target_ref' do
      expect(repository.ref_exists?(target_ref)).to be(false)

      commit_sha = merge_to_ref
      ref_head = repository.commit(target_ref)

      expect(commit_sha).to be_present
      expect(repository.ref_exists?(target_ref)).to be(true)
      expect(ref_head.id).to eq(commit_sha)
    end

    it 'does not change the right branch HEAD' do
      expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target }
    end
  end

1749
  describe '#merge' do
1750
    let(:repository) { mutable_repository }
1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761
    let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
    let(:target_branch) { 'test-merge-target-branch' }

    before do
      repository.create_branch(target_branch, '6d394385cf567f80a8fd85055db1ab4c5295806f')
    end

    after do
      ensure_seeds
    end

1762 1763 1764 1765
    it 'can perform a merge' do
      merge_commit_id = nil
      result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id|
        merge_commit_id = commit_id
1766
      end
1767

1768 1769 1770 1771
      expect(result.newrev).to eq(merge_commit_id)
      expect(result.repo_created).to eq(false)
      expect(result.branch_created).to eq(false)
    end
1772

1773 1774 1775 1776 1777
    it 'returns nil if there was a concurrent branch update' do
      concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
      result = repository.merge(user, source_sha, target_branch, 'Test merge') do
        # This ref update should make the merge fail
        repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id)
1778
      end
1779

1780 1781
      # This 'nil' signals that the merge was not applied
      expect(result).to be_nil
1782

1783 1784
      # Our concurrent ref update should not have been undone
      expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id)
1785 1786 1787
    end
  end

1788
  describe '#ff_merge' do
1789
    let(:repository) { mutable_repository }
1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803
    let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
    let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
    let(:target_branch) { 'test-ff-target-branch' }

    before do
      repository.create_branch(target_branch, branch_head)
    end

    after do
      ensure_seeds
    end

    subject { repository.ff_merge(user, source_sha, target_branch) }

1804 1805 1806 1807 1808
    shared_examples '#ff_merge' do
      it 'performs a ff_merge' do
        expect(subject.newrev).to eq(source_sha)
        expect(subject.repo_created).to be(false)
        expect(subject.branch_created).to be(false)
1809

1810 1811
        expect(repository.commit(target_branch).id).to eq(source_sha)
      end
1812

1813 1814
      context 'with a non-existing target branch' do
        subject { repository.ff_merge(user, source_sha, 'this-isnt-real') }
1815

1816 1817 1818
        it 'throws an ArgumentError' do
          expect { subject }.to raise_error(ArgumentError)
        end
1819 1820
      end

1821 1822
      context 'with a non-existing source commit' do
        let(:source_sha) { 'f001' }
1823

1824 1825 1826
        it 'throws an ArgumentError' do
          expect { subject }.to raise_error(ArgumentError)
        end
1827 1828
      end

1829 1830 1831 1832 1833 1834 1835 1836 1837 1838
      context 'when the source sha is not a descendant of the branch head' do
        let(:source_sha) { '1a0b36b3cdad1d2ee32457c102a8c0b7056fa863' }

        it "doesn't perform the ff_merge" do
          expect { subject }.to raise_error(Gitlab::Git::CommitError)

          expect(repository.commit(target_branch).id).to eq(branch_head)
        end
      end
    end
1839

1840 1841 1842 1843
    it "calls Gitaly's OperationService" do
      expect_any_instance_of(Gitlab::GitalyClient::OperationService)
        .to receive(:user_ff_branch).with(user, source_sha, target_branch)
        .and_return(nil)
1844

1845
      subject
1846 1847
    end

1848
    it_behaves_like '#ff_merge'
1849 1850
  end

1851
  describe '#delete_all_refs_except' do
1852
    let(:repository) { mutable_repository }
1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874

    before do
      repository.write_ref("refs/delete/a", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
      repository.write_ref("refs/also-delete/b", "12d65c8dd2b2676fa3ac47d955accc085a37a9c1")
      repository.write_ref("refs/keep/c", "6473c90867124755509e100d0d35ebdc85a0b6ae")
      repository.write_ref("refs/also-keep/d", "0b4bc9a49b562e85de7cc9e834518ea6828729b9")
    end

    after do
      ensure_seeds
    end

    it 'deletes all refs except those with the specified prefixes' do
      repository.delete_all_refs_except(%w(refs/keep refs/also-keep refs/heads))
      expect(repository.ref_exists?("refs/delete/a")).to be(false)
      expect(repository.ref_exists?("refs/also-delete/b")).to be(false)
      expect(repository.ref_exists?("refs/keep/c")).to be(true)
      expect(repository.ref_exists?("refs/also-keep/d")).to be(true)
      expect(repository.ref_exists?("refs/heads/master")).to be(true)
    end
  end

1875
  describe 'remotes' do
1876
    let(:repository) { mutable_repository }
1877
    let(:remote_name) { 'my-remote' }
1878
    let(:url) { 'http://my-repo.git' }
1879 1880 1881 1882 1883 1884 1885 1886

    after do
      ensure_seeds
    end

    describe '#add_remote' do
      let(:mirror_refmap) { '+refs/*:refs/*' }

1887 1888
      it 'added the remote' do
        begin
1889
          repository_rugged.remotes.delete(remote_name)
1890
        rescue Rugged::ConfigError
1891
        end
1892

1893
        repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
1894

1895 1896 1897 1898
        expect(repository_rugged.remotes[remote_name]).not_to be_nil
        expect(repository_rugged.config["remote.#{remote_name}.mirror"]).to eq('true')
        expect(repository_rugged.config["remote.#{remote_name}.prune"]).to eq('true')
        expect(repository_rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap)
1899 1900 1901 1902
      end
    end

    describe '#remove_remote' do
1903
      it 'removes the remote' do
1904
        repository_rugged.remotes.create(remote_name, url)
1905

1906
        repository.remove_remote(remote_name)
1907

1908
        expect(repository_rugged.remotes[remote_name]).to be_nil
1909 1910 1911 1912
      end
    end
  end

1913
  describe '#bundle_to_disk' do
1914
    let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
1915

1916 1917
    after do
      FileUtils.rm_rf(save_path)
1918 1919
    end

1920 1921
    it 'saves a bundle to disk' do
      repository.bundle_to_disk(save_path)
1922

1923 1924 1925 1926 1927
      success = system(
        *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}),
        [:out, :err] => '/dev/null'
      )
      expect(success).to be true
1928 1929 1930
    end
  end

1931
  describe '#create_from_bundle' do
1932 1933
    let(:valid_bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
    let(:malicious_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') }
1934 1935
    let(:project) { create(:project) }
    let(:imported_repo) { project.repository.raw }
1936

1937
    before do
1938
      expect(repository.bundle_to_disk(valid_bundle_path)).to be_truthy
1939
    end
1940

1941
    after do
1942
      FileUtils.rm_rf(valid_bundle_path)
1943
    end
1944

1945 1946
    it 'creates a repo from a bundle file' do
      expect(imported_repo).not_to exist
1947

1948
      result = imported_repo.create_from_bundle(valid_bundle_path)
1949

1950 1951 1952
      expect(result).to be_truthy
      expect(imported_repo).to exist
      expect { imported_repo.fsck }.not_to raise_exception
1953 1954
    end

1955
    it 'creates a symlink to the global hooks dir' do
1956
      imported_repo.create_from_bundle(valid_bundle_path)
1957
      hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
1958

1959
      expect(File.readlink(hooks_path)).to eq(Gitlab::Shell.new.hooks_path)
1960
    end
1961 1962 1963 1964 1965 1966

    it 'raises an error if the bundle is an attempted malicious payload' do
      expect do
        imported_repo.create_from_bundle(malicious_bundle_path)
      end.to raise_error(::Gitlab::Git::BundleFile::InvalidBundleError)
    end
1967 1968
  end

1969
  describe '#checksum' do
1970
    it 'calculates the checksum for non-empty repo' do
1971
      expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059'
1972
    end
1973

1974 1975
    it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
      FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
1976

1977 1978 1979 1980
      system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
             chdir: storage_path,
             out:   '/dev/null',
             err:   '/dev/null')
1981

1982
      empty_repo = described_class.new('default', 'empty-repo.git', '', 'group/empty-repo')
1983

1984 1985
      expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
    end
1986

1987 1988
    it 'raises Gitlab::Git::Repository::InvalidRepository error for non-valid git repo' do
      FileUtils.rm_rf(File.join(storage_path, 'non-valid.git'))
1989

1990 1991 1992 1993
      system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} non-valid.git),
             chdir: SEED_STORAGE_PATH,
             out: '/dev/null',
             err: '/dev/null')
1994

1995
      File.truncate(File.join(storage_path, 'non-valid.git/HEAD'), 0)
1996

1997
      non_valid = described_class.new('default', 'non-valid.git', '', 'a/non-valid')
1998

1999
      expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
2000 2001
    end

2002
    it 'raises Gitlab::Git::Repository::NoRepository error when there is no repo' do
2003
      broken_repo = described_class.new('default', 'a/path.git', '', 'a/path')
2004

2005
      expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
2006 2007 2008
    end
  end

2009 2010
  describe '#clean_stale_repository_files' do
    let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') }
2011

2012 2013 2014
    it 'cleans up the files' do
      create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master]
      raise 'preparation failed' unless system(*create_worktree, err: '/dev/null')
2015

2016 2017 2018 2019
      FileUtils.touch(worktree_path, mtime: Time.now - 8.hours)
      # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object,
      # but the HEAD must be 40 characters long or git will ignore it.
      File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA)
2020

2021 2022
      # git 2.16 fails with "fatal: bad object HEAD"
      expect(rev_list_all).to be false
2023

2024
      repository.clean_stale_repository_files
2025

2026 2027
      expect(rev_list_all).to be true
      expect(File.exist?(worktree_path)).to be_falsey
2028 2029
    end

2030 2031
    def rev_list_all
      system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null')
2032 2033
    end

2034 2035
    it 'increments a counter upon an error' do
      expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError)
2036

2037
      counter = double(:counter)
2038

2039 2040 2041
      expect(counter).to receive(:increment)
      expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total,
                                                        'Number of failed repository cleanup events').and_return(counter)
2042

2043
      repository.clean_stale_repository_files
2044
    end
2045
  end
2046

2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076
  describe '#squash' do
    let(:squash_id) { '1' }
    let(:branch_name) { 'fix' }
    let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
    let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }

    subject do
      opts = {
        branch: branch_name,
        start_sha: start_sha,
        end_sha: end_sha,
        author: user,
        message: 'Squash commit message'
      }

      repository.squash(user, squash_id, opts)
    end

    # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234
    skip 'sparse checkout' do
      let(:expected_files) { %w(files files/js files/js/application.js) }

      it 'checks out only the files in the diff' do
        allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
          m.call(*args) do
            worktree_path = args[0]
            files_pattern = File.join(worktree_path, '**', '*')
            expected = expected_files.map do |path|
              File.expand_path(path, worktree_path)
            end
2077

2078 2079 2080
            expect(Dir[files_pattern]).to eq(expected)
          end
        end
2081

2082
        subject
2083 2084
      end

2085
      context 'when the diff contains a rename' do
2086
        let(:end_sha) { new_commit_move_file(repository_rugged).oid }
2087 2088 2089

        after do
          # Erase our commits so other tests get the original repo
2090
          repository_rugged.references.update('refs/heads/master', SeedRepo::LastCommit::ID)
2091
        end
2092

2093
        it 'does not include the renamed file in the sparse checkout' do
2094 2095 2096 2097 2098
          allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
            m.call(*args) do
              worktree_path = args[0]
              files_pattern = File.join(worktree_path, '**', '*')

2099 2100
              expect(Dir[files_pattern]).not_to include('CHANGELOG')
              expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG')
2101 2102 2103 2104 2105 2106
            end
          end

          subject
        end
      end
2107
    end
2108

2109 2110 2111
    # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234
    skip 'with an ASCII-8BIT diff' do
      let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+✓ testme\n ======\n \n Sample repo for testing gitlab features\n" }
2112

2113 2114 2115
      it 'applies a ASCII-8BIT diff' do
        allow(repository).to receive(:run_git!).and_call_original
        allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
2116

2117
        expect(subject).to match(/\h{40}/)
2118
      end
2119
    end
2120

2121 2122 2123
    # Should be ported to gitaly-ruby rspec suite https://gitlab.com/gitlab-org/gitaly/issues/1234
    skip 'with trailing whitespace in an invalid patch' do
      let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+   \n ======   \n \n Sample repo for testing gitlab features\n" }
2124

2125 2126 2127
      it 'does not include whitespace warnings in the error' do
        allow(repository).to receive(:run_git!).and_call_original
        allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
2128

2129 2130 2131
        expect { subject }.to raise_error do |error|
          expect(error).to be_a(described_class::GitError)
          expect(error.message).not_to include('trailing whitespace')
2132 2133
        end
      end
2134
    end
2135 2136
  end

2137
  def create_remote_branch(remote_name, branch_name, source_branch_name)
2138
    source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
2139
    repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
Robert Speicher's avatar
Robert Speicher committed
2140 2141 2142
  end

  # Build the options hash that's passed to Rugged::Commit#create
2143
  def commit_options(repo, index, target, ref, message)
Robert Speicher's avatar
Robert Speicher committed
2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156
    options = {}
    options[:tree] = index.write_tree(repo)
    options[:author] = {
      email: "test@example.com",
      name: "Test Author",
      time: Time.gm(2014, "mar", 3, 20, 15, 1)
    }
    options[:committer] = {
      email: "test@example.com",
      name: "Test Author",
      time: Time.gm(2014, "mar", 3, 20, 15, 1)
    }
    options[:message] ||= message
2157 2158
    options[:parents] = repo.empty? ? [] : [target].compact
    options[:update_ref] = ref
Robert Speicher's avatar
Robert Speicher committed
2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173

    options
  end

  # Writes a new commit to the repo and returns a Rugged::Commit.  Replaces the
  # contents of CHANGELOG with a single new line of text.
  def new_commit_edit_old_file(repo)
    oid = repo.write("I replaced the changelog with this text", :blob)
    index = repo.index
    index.read_tree(repo.head.target.tree)
    index.add(path: "CHANGELOG", oid: oid, mode: 0100644)

    options = commit_options(
      repo,
      index,
2174 2175
      repo.head.target,
      "HEAD",
Robert Speicher's avatar
Robert Speicher committed
2176 2177 2178 2179 2180 2181 2182 2183
      "Edit CHANGELOG in its original location"
    )

    sha = Rugged::Commit.create(repo, options)
    repo.lookup(sha)
  end

  # Writes a new commit to the repo and returns a Rugged::Commit.  Replaces the
2184 2185 2186
  # contents of the specified file_path with new text.
  def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
    oid = repo.write(text, :blob)
Robert Speicher's avatar
Robert Speicher committed
2187
    index = repo.index
2188 2189 2190
    index.read_tree(branch.target.tree)
    index.add(path: file_path, oid: oid, mode: 0100644)
    options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
Robert Speicher's avatar
Robert Speicher committed
2191 2192 2193 2194
    sha = Rugged::Commit.create(repo, options)
    repo.lookup(sha)
  end

2195 2196 2197 2198 2199 2200 2201
  # Writes a new commit to the repo and returns a Rugged::Commit.  Replaces the
  # contents of encoding/CHANGELOG with new text.
  def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
    branch = repo.branches[branch_name]
    new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
  end

Robert Speicher's avatar
Robert Speicher committed
2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212
  # Writes a new commit to the repo and returns a Rugged::Commit.  Moves the
  # CHANGELOG file to the encoding/ directory.
  def new_commit_move_file(repo)
    blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
    file_content = repo.lookup(blob_oid).content
    oid = repo.write(file_content, :blob)
    index = repo.index
    index.read_tree(repo.head.target.tree)
    index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
    index.remove("CHANGELOG")

2213
    options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
Robert Speicher's avatar
Robert Speicher committed
2214 2215 2216 2217

    sha = Rugged::Commit.create(repo, options)
    repo.lookup(sha)
  end
2218 2219 2220 2221 2222 2223

  def refs(dir)
    IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
      line.split("\t").last
    end
  end
Robert Speicher's avatar
Robert Speicher committed
2224
end