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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

86
    it 'gets the branch name from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
87
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:default_branch_name)
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
102 103

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

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

110
    it 'gets the branch names from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
111
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:branch_names)
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 }
122

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

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

Robert Speicher's avatar
Robert Speicher committed
131 132 133 134 135 136
    describe '#last' do
      subject { super().last }
      it { is_expected.to eq("v1.2.1") }
    end
    it { is_expected.to include("v1.0.0") }
    it { is_expected.not_to include("v5.0.0") }
137

138
    it 'gets the tag names from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
139
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:tag_names)
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 1002

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

      after do
        ensure_seeds
      end

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

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

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

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

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

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

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

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

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

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

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

1057 1058
      after do
        ensure_seeds
1059
      end
1060

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

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

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

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

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

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

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

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

      expect(collection).to be_a(Gitlab::Git::DiffStatsCollection)
      expect(collection).to be_a(Enumerable)
      expect(collection.to_a).to be_empty
    end
1096 1097

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

1101 1102 1103 1104 1105 1106
      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
1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117

    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
1118 1119
  end

Robert Speicher's avatar
Robert Speicher committed
1120 1121
  describe "#ls_files" do
    let(:master_file_paths) { repository.ls_files("master") }
1122
    let(:utf8_file_paths) { repository.ls_files("ls-files-utf8") }
Robert Speicher's avatar
Robert Speicher committed
1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
    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

1133
    it "does not read submodule directory and empty directory of master branch" do
Robert Speicher's avatar
Robert Speicher committed
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143
      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
1144 1145 1146 1147

    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
1148 1149 1150
  end

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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1153 1154 1155
    after do
      FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
    end
Robert Speicher's avatar
Robert Speicher committed
1156

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1157 1158 1159
    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
1160

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

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1164 1165 1166
      before do
        repository.create_branch(branch_name, "master")
      end
Robert Speicher's avatar
Robert Speicher committed
1167

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1168 1169
      after do
        repository.rm_branch(branch_name, user: build(:admin))
Robert Speicher's avatar
Robert Speicher committed
1170 1171
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1172 1173
      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
1174

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1175
        repository
Robert Speicher's avatar
Robert Speicher committed
1176
      end
Jacob Vosmaer's avatar
Jacob Vosmaer committed
1177
    end
Robert Speicher's avatar
Robert Speicher committed
1178

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1179 1180 1181 1182
    context "with no .gitattrbutes" do
      before do
        repository.copy_gitattributes("master")
      end
Robert Speicher's avatar
Robert Speicher committed
1183

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1184 1185 1186 1187
      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
1188

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1189 1190 1191
    context "with .gitattrbutes" do
      before do
        repository.copy_gitattributes("gitattributes")
Robert Speicher's avatar
Robert Speicher committed
1192 1193
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1194 1195 1196
      it "has an info/attributes" do
        expect(File.exist?(attributes_path)).to be_truthy
      end
1197

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1198 1199 1200 1201 1202
      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
1203

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1204 1205 1206 1207
    context "with updated .gitattrbutes" do
      before do
        repository.copy_gitattributes("gitattributes")
        repository.copy_gitattributes("gitattributes-updated")
Robert Speicher's avatar
Robert Speicher committed
1208 1209
      end

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1210 1211 1212
      it "has an info/attributes" do
        expect(File.exist?(attributes_path)).to be_truthy
      end
1213

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1214 1215 1216
      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
1217 1218
      end
    end
1219

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1220 1221 1222 1223 1224
    context "with no .gitattrbutes in HEAD but with previous info/attributes" do
      before do
        repository.copy_gitattributes("gitattributes")
        repository.copy_gitattributes("master")
      end
1225

Jacob Vosmaer's avatar
Jacob Vosmaer committed
1226 1227 1228
      it "does not have an info/attributes" do
        expect(File.exist?(attributes_path)).to be_falsey
      end
1229
    end
Robert Speicher's avatar
Robert Speicher committed
1230 1231
  end

1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259
  describe '#gitattribute' do
    let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '') }

    after do
      ensure_seeds
    end

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

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

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

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

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

1260
  describe '#ref_exists?' do
1261 1262 1263
    it 'returns true for an existing tag' do
      expect(repository.ref_exists?('refs/heads/master')).to eq(true)
    end
1264

1265 1266
    it 'returns false for a non-existing tag' do
      expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false)
1267 1268
    end

1269 1270
    it 'raises an ArgumentError for an empty string' do
      expect { repository.ref_exists?('') }.to raise_error(ArgumentError)
1271 1272
    end

1273 1274
    it 'raises an ArgumentError for an invalid ref' do
      expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError)
1275 1276 1277
    end
  end

Robert Speicher's avatar
Robert Speicher committed
1278
  describe '#tag_exists?' do
1279 1280
    it 'returns true for an existing tag' do
      tag = repository.tag_names.first
1281

1282
      expect(repository.tag_exists?(tag)).to eq(true)
1283 1284
    end

1285 1286
    it 'returns false for a non-existing tag' do
      expect(repository.tag_exists?('v9000')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1287 1288 1289 1290
    end
  end

  describe '#branch_exists?' do
1291 1292
    it 'returns true for an existing branch' do
      expect(repository.branch_exists?('master')).to eq(true)
Robert Speicher's avatar
Robert Speicher committed
1293 1294
    end

1295 1296
    it 'returns false for a non-existing branch' do
      expect(repository.branch_exists?('kittens')).to eq(false)
Robert Speicher's avatar
Robert Speicher committed
1297 1298
    end

1299 1300
    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
1301 1302 1303 1304
    end
  end

  describe '#local_branches' do
1305 1306 1307 1308 1309
    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
1310 1311
    end

1312
    after do
Robert Speicher's avatar
Robert Speicher committed
1313 1314 1315 1316
      ensure_seeds
    end

    it 'returns the local branches' do
1317 1318
      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
1319
    end
1320

1321
    it 'returns a Branch with UTF-8 fields' do
1322
      branches = repository.local_branches.to_a
1323 1324
      expect(branches.size).to be > 0
      branches.each do |branch|
1325 1326
        expect(branch.name).to be_utf8
        expect(branch.target).to be_utf8 unless branch.target.nil?
1327
      end
1328
    end
1329

1330
    it 'gets the branches from GitalyClient' do
Andrew Newdigate's avatar
Andrew Newdigate committed
1331
      expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:local_branches)
1332
        .and_return([])
1333
      repository.local_branches
1334
    end
1335

Andrew Newdigate's avatar
Andrew Newdigate committed
1336
    it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RefService, :local_branches do
1337
      subject { repository.local_branches }
1338
    end
Robert Speicher's avatar
Robert Speicher committed
1339 1340
  end

1341
  describe '#languages' do
1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355
    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]
1356

1357 1358
        expect(a.keys.sort).to eq(b.keys.sort)
        expect(a[:value]).to be_within(0.1).of(b[:value])
1359

1360 1361
        non_float_keys = a.keys - [:value]
        expect(a.values_at(*non_float_keys)).to eq(b.values_at(*non_float_keys))
1362 1363 1364
      end
    end

1365 1366
    it "uses the repository's HEAD when no ref is passed" do
      lang = repository.languages.first
1367

1368
      expect(lang[:label]).to eq('Ruby')
1369 1370 1371
    end
  end

1372
  describe '#license_short_name' do
1373
    subject { repository.license_short_name }
1374

1375 1376 1377
    context 'when no license file can be found' do
      let(:project) { create(:project, :repository) }
      let(:repository) { project.repository.raw_repository }
1378

1379 1380
      before do
        project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
1381 1382
      end

1383
      it { is_expected.to be_nil }
1384 1385
    end

1386 1387
    context 'when an mit license is found' do
      it { is_expected.to eq('mit') }
1388 1389 1390
    end
  end

1391
  describe '#fetch_source_branch!' do
1392
    let(:local_ref) { 'refs/merge-requests/1/head' }
1393
    let(:source_repository) { mutable_repository }
1394

1395 1396 1397
    after do
      ensure_seeds
    end
1398

1399 1400 1401
    context 'when the branch exists' do
      context 'when the commit does not exist locally' do
        let(:source_branch) { 'new-branch-for-fetch-source-branch' }
1402 1403
        let(:source_path) { File.join(TestEnv.repos_path, source_repository.relative_path) }
        let(:source_rugged) { Rugged::Repository.new(source_path) }
1404
        let(:new_oid) { new_commit_edit_old_file(source_rugged).oid }
1405

1406 1407
        before do
          source_rugged.branches.create(source_branch, new_oid)
1408
        end
1409

1410 1411 1412
        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)
1413
        end
1414 1415
      end

1416 1417 1418 1419 1420 1421 1422
      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
1423

1424 1425
          expect(repository.fetch_source_branch!(source_repository, source_branch, local_ref)).to eq(true)
          expect(repository.commit(local_ref).sha).to eq(expected_oid)
1426
        end
1427 1428
      end
    end
1429

1430 1431
    context 'when the branch does not exist' do
      let(:source_branch) { 'definitely-not-master' }
1432

1433 1434 1435 1436
      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
1437
    end
1438 1439
  end

1440
  describe '#rm_branch' do
1441 1442 1443
    let(:project) { create(:project, :repository) }
    let(:repository) { project.repository.raw }
    let(:branch_name) { "to-be-deleted-soon" }
1444

1445 1446 1447
    before do
      project.add_developer(user)
      repository.create_branch(branch_name)
1448 1449
    end

1450 1451
    it "removes the branch from the repo" do
      repository.rm_branch(branch_name, user: user)
1452

1453
      expect(repository_rugged.branches[branch_name]).to be_nil
1454 1455 1456
    end
  end

1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471
  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
1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484

    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
1485 1486
  end

1487 1488
  describe '#write_config' do
    before do
1489
      repository_rugged.config["gitlab.fullpath"] = repository_path
1490 1491
    end

1492 1493 1494
    context 'is given a path' do
      it 'writes it to disk' do
        repository.write_config(full_path: "not-the/real-path.git")
1495

1496
        config = File.read(File.join(repository_path, "config"))
1497

1498 1499
        expect(config).to include("[gitlab]")
        expect(config).to include("fullpath = not-the/real-path.git")
1500
      end
1501
    end
1502

1503 1504 1505
    context 'it is given an empty path' do
      it 'does not write it to disk' do
        repository.write_config(full_path: "")
1506

1507
        config = File.read(File.join(repository_path, "config"))
1508

1509 1510
        expect(config).to include("[gitlab]")
        expect(config).to include("fullpath = #{repository_path}")
1511
      end
1512
    end
1513

1514 1515 1516
    context 'repository does not exist' do
      it 'raises NoRepository and does not call Gitaly WriteConfig' do
        repository = Gitlab::Git::Repository.new('default', 'does/not/exist.git', '')
1517

1518
        expect(repository.gitaly_repository_client).not_to receive(:write_config)
1519

1520 1521 1522
        expect do
          repository.write_config(full_path: 'foo/bar.git')
        end.to raise_error(Gitlab::Git::Repository::NoRepository)
1523
      end
1524 1525 1526
    end
  end

1527
  describe '#set_config' do
1528
    let(:repository) { mutable_repository }
1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539
    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

1540 1541 1542
      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')
1543 1544 1545
    end

    after do
1546
      entries.keys.each { |k| repository_rugged.config.delete(k) }
1547 1548 1549 1550
    end
  end

  describe '#delete_config' do
1551
    let(:repository) { mutable_repository }
1552 1553 1554 1555 1556 1557 1558 1559 1560 1561
    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|
1562
        repository_rugged.config[key] = value
1563 1564 1565 1566
      end

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

1567
      config_keys = repository_rugged.config.each_key.to_a
1568 1569 1570 1571 1572
      expect(config_keys).not_to include('test.foo1')
      expect(config_keys).not_to include('test.foo2')
    end
  end

1573
  describe '#merge' do
1574
    let(:repository) { mutable_repository }
1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585
    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

1586 1587 1588 1589
    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
1590
      end
1591

1592 1593 1594 1595
      expect(result.newrev).to eq(merge_commit_id)
      expect(result.repo_created).to eq(false)
      expect(result.branch_created).to eq(false)
    end
1596

1597 1598 1599 1600 1601
    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)
1602
      end
1603

1604 1605
      # This 'nil' signals that the merge was not applied
      expect(result).to be_nil
1606

1607 1608
      # Our concurrent ref update should not have been undone
      expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id)
1609 1610 1611
    end
  end

1612
  describe '#ff_merge' do
1613
    let(:repository) { mutable_repository }
1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627
    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) }

1628 1629 1630 1631 1632
    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)
1633

1634 1635
        expect(repository.commit(target_branch).id).to eq(source_sha)
      end
1636

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

1640 1641 1642
        it 'throws an ArgumentError' do
          expect { subject }.to raise_error(ArgumentError)
        end
1643 1644
      end

1645 1646
      context 'with a non-existing source commit' do
        let(:source_sha) { 'f001' }
1647

1648 1649 1650
        it 'throws an ArgumentError' do
          expect { subject }.to raise_error(ArgumentError)
        end
1651 1652
      end

1653 1654 1655 1656 1657 1658 1659 1660 1661 1662
      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
1663

1664 1665 1666 1667
    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)
1668

1669
      subject
1670 1671
    end

1672
    it_behaves_like '#ff_merge'
1673 1674
  end

1675
  describe '#delete_all_refs_except' do
1676
    let(:repository) { mutable_repository }
1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698

    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

1699
  describe 'remotes' do
1700
    let(:repository) { mutable_repository }
1701
    let(:remote_name) { 'my-remote' }
1702
    let(:url) { 'http://my-repo.git' }
1703 1704 1705 1706 1707 1708 1709 1710

    after do
      ensure_seeds
    end

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

1711 1712
      it 'added the remote' do
        begin
1713
          repository_rugged.remotes.delete(remote_name)
1714
        rescue Rugged::ConfigError
1715
        end
1716

1717
        repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap)
1718

1719 1720 1721 1722
        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)
1723 1724 1725 1726
      end
    end

    describe '#remove_remote' do
1727
      it 'removes the remote' do
1728
        repository_rugged.remotes.create(remote_name, url)
1729

1730
        repository.remove_remote(remote_name)
1731

1732
        expect(repository_rugged.remotes[remote_name]).to be_nil
1733 1734 1735 1736
      end
    end
  end

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

1740 1741
    after do
      FileUtils.rm_rf(save_path)
1742 1743
    end

1744 1745
    it 'saves a bundle to disk' do
      repository.bundle_to_disk(save_path)
1746

1747 1748 1749 1750 1751
      success = system(
        *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}),
        [:out, :err] => '/dev/null'
      )
      expect(success).to be true
1752 1753 1754
    end
  end

1755
  describe '#create_from_bundle' do
1756 1757
    let(:valid_bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
    let(:malicious_bundle_path) { Rails.root.join('spec/fixtures/malicious.bundle') }
1758 1759
    let(:project) { create(:project) }
    let(:imported_repo) { project.repository.raw }
1760

1761
    before do
1762
      expect(repository.bundle_to_disk(valid_bundle_path)).to be_truthy
1763
    end
1764

1765
    after do
1766
      FileUtils.rm_rf(valid_bundle_path)
1767
    end
1768

1769 1770
    it 'creates a repo from a bundle file' do
      expect(imported_repo).not_to exist
1771

1772
      result = imported_repo.create_from_bundle(valid_bundle_path)
1773

1774 1775 1776
      expect(result).to be_truthy
      expect(imported_repo).to exist
      expect { imported_repo.fsck }.not_to raise_exception
1777 1778
    end

1779
    it 'creates a symlink to the global hooks dir' do
1780
      imported_repo.create_from_bundle(valid_bundle_path)
1781
      hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
1782

1783
      expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
1784
    end
1785 1786 1787 1788 1789 1790

    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
1791 1792
  end

1793
  describe '#checksum' do
1794
    it 'calculates the checksum for non-empty repo' do
1795
      expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059'
1796
    end
1797

1798 1799
    it 'returns 0000000000000000000000000000000000000000 for an empty repo' do
      FileUtils.rm_rf(File.join(storage_path, 'empty-repo.git'))
1800

1801 1802 1803 1804
      system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
             chdir: storage_path,
             out:   '/dev/null',
             err:   '/dev/null')
1805

1806
      empty_repo = described_class.new('default', 'empty-repo.git', '')
1807

1808 1809
      expect(empty_repo.checksum).to eq '0000000000000000000000000000000000000000'
    end
1810

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

1814 1815 1816 1817
      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')
1818

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

1821
      non_valid = described_class.new('default', 'non-valid.git', '')
1822

1823
      expect { non_valid.checksum }.to raise_error(Gitlab::Git::Repository::InvalidRepository)
1824 1825
    end

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

1829
      expect { broken_repo.checksum }.to raise_error(Gitlab::Git::Repository::NoRepository)
1830 1831 1832
    end
  end

1833 1834
  describe '#clean_stale_repository_files' do
    let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') }
1835

1836 1837 1838
    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')
1839

1840 1841 1842 1843
      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)
1844

1845 1846
      # git 2.16 fails with "fatal: bad object HEAD"
      expect(rev_list_all).to be false
1847

1848
      repository.clean_stale_repository_files
1849

1850 1851
      expect(rev_list_all).to be true
      expect(File.exist?(worktree_path)).to be_falsey
1852 1853
    end

1854 1855
    def rev_list_all
      system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null')
1856 1857
    end

1858 1859
    it 'increments a counter upon an error' do
      expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError)
1860

1861
      counter = double(:counter)
1862

1863 1864 1865
      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)
1866

1867
      repository.clean_stale_repository_files
1868
    end
1869
  end
1870

1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900
  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
1901

1902 1903 1904
            expect(Dir[files_pattern]).to eq(expected)
          end
        end
1905

1906
        subject
1907 1908
      end

1909
      context 'when the diff contains a rename' do
1910
        let(:end_sha) { new_commit_move_file(repository_rugged).oid }
1911 1912 1913

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

1917
        it 'does not include the renamed file in the sparse checkout' do
1918 1919 1920 1921 1922
          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, '**', '*')

1923 1924
              expect(Dir[files_pattern]).not_to include('CHANGELOG')
              expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG')
1925 1926 1927 1928 1929 1930
            end
          end

          subject
        end
      end
1931
    end
1932

1933 1934 1935
    # 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" }
1936

1937 1938 1939
      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'))
1940

1941
        expect(subject).to match(/\h{40}/)
1942
      end
1943
    end
1944

1945 1946 1947
    # 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" }
1948

1949 1950 1951
      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'))
1952

1953 1954 1955
        expect { subject }.to raise_error do |error|
          expect(error).to be_a(described_class::GitError)
          expect(error.message).not_to include('trailing whitespace')
1956 1957
        end
      end
1958
    end
1959 1960
  end

1961
  def create_remote_branch(remote_name, branch_name, source_branch_name)
1962
    source_branch = repository.branches.find { |branch| branch.name == source_branch_name }
1963
    repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
Robert Speicher's avatar
Robert Speicher committed
1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034
  end

  # Build the options hash that's passed to Rugged::Commit#create
  def commit_options(repo, index, message)
    options = {}
    options[:tree] = index.write_tree(repo)
    options[:author] = {
      email: "test@example.com",
      name: "Test Author",
      time: Time.gm(2014, "mar", 3, 20, 15, 1)
    }
    options[:committer] = {
      email: "test@example.com",
      name: "Test Author",
      time: Time.gm(2014, "mar", 3, 20, 15, 1)
    }
    options[:message] ||= message
    options[:parents] = repo.empty? ? [] : [repo.head.target].compact
    options[:update_ref] = "HEAD"

    options
  end

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

    options = commit_options(
      repo,
      index,
      "Edit CHANGELOG in its original location"
    )

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

  # Writes a new commit to the repo and returns a Rugged::Commit.  Replaces the
  # contents of encoding/CHANGELOG with new text.
  def new_commit_edit_new_file(repo)
    oid = repo.write("I'm a new changelog with different text", :blob)
    index = repo.index
    index.read_tree(repo.head.target.tree)
    index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)

    options = commit_options(repo, index, "Edit encoding/CHANGELOG")

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

  # Writes a new commit to the repo and returns a Rugged::Commit.  Moves the
  # CHANGELOG file to the encoding/ directory.
  def new_commit_move_file(repo)
    blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
    file_content = repo.lookup(blob_oid).content
    oid = repo.write(file_content, :blob)
    index = repo.index
    index.read_tree(repo.head.target.tree)
    index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
    index.remove("CHANGELOG")

    options = commit_options(repo, index, "Move CHANGELOG to encoding/")

    sha = Rugged::Commit.create(repo, options)
    repo.lookup(sha)
  end
2035 2036 2037 2038 2039 2040

  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
2041
end